Merge branch 'develop' into ios-192-profile-about

# Conflicts:
#	Mastodon.xcodeproj/project.pbxproj
#	Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift
#	Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift
#	Mastodon/Protocol/Provider/DataSourceFacade+Status.swift
#	Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift
#	Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift
#	Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift
#	Mastodon/Protocol/Provider/DataSourceProvider.swift
#	Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift
#	Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift
#	Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift
#	Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift
#	Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift
#	Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift
#	Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift
#	Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift
#	Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift
#	Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift
#	Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift
#	Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift
#	Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift
#	Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift
#	Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift
#	Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift
#	Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift
#	Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift
#	Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift
#	Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift
#	Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift
#	Mastodon/Scene/Profile/MeProfileViewModel.swift
#	Mastodon/Scene/Profile/ProfileViewController.swift
#	Mastodon/Scene/Profile/ProfileViewModel.swift
#	Mastodon/Scene/Profile/RemoteProfileViewModel.swift
#	Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift
#	Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift
#	Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
#	Mastodon/Scene/Report/Report/ReportViewModel.swift
#	Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift
#	Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift
#	Mastodon/Scene/Root/MainTab/MainTabBarController.swift
#	Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift
#	Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift
#	Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift
#	Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift
#	Mastodon/Scene/Thread/ThreadViewModel.swift
#	MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift
#	MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift
#	MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift
#	MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift
#	MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift
#	MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift
#	MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift
#	MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift
#	MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift
#	MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift
#	MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift
This commit is contained in:
Nathan Mattes 2024-01-08 22:32:57 +01:00
commit febbc6f22a
51 changed files with 343 additions and 363 deletions

View File

@ -23,7 +23,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
}
switch item {
case .account(let account, relationship: _):
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
@ -36,22 +36,21 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
tag: tag
)
case .notification(let notification):
let managedObjectContext = context.managedObjectContext
let _status: MastodonStatus? = notification.status
if let status = _status {
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
target: .status, // remove reblog wrapper
status: status
)
} else {
await DataSourceFacade.coordinateToProfileScene(
provider: self,
account: notification.entity.account)
}
} // end Task
} // end func
account: notification.entity.account
)
} // end Task
} // end func
}
}
}

View File

@ -29,12 +29,12 @@ extension DiscoveryCommunityViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.statusFetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.statusFetchedResultsController.setRecords(
viewModel.statusFetchedResultsController.records.filter { $0.id != status.id }
viewModel.dataController.setRecords(
viewModel.dataController.records.filter { $0.id != status.id }
)
}

View File

@ -29,7 +29,7 @@ extension DiscoveryCommunityViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -145,7 +145,7 @@ extension DiscoveryCommunityViewModel.State {
self.maxID = newMaxID
var hasNewStatusesAppend = false
var statusIDs = isReloading ? [] : await viewModel.statusFetchedResultsController.records
var statusIDs = isReloading ? [] : await viewModel.dataController.records
for status in response.value {
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
statusIDs.append(.fromEntity(status))
@ -158,7 +158,7 @@ extension DiscoveryCommunityViewModel.State {
} else {
await enter(state: NoMore.self)
}
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
await viewModel.dataController.setRecords(statusIDs)
viewModel.didLoadLatest.send()
} catch {

View File

@ -21,7 +21,7 @@ final class DiscoveryCommunityViewModel {
let context: AppContext
let authContext: AuthContext
let viewDidAppeared = PassthroughSubject<Void, Never>()
let statusFetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -45,7 +45,7 @@ final class DiscoveryCommunityViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController()
self.dataController = StatusDataController()
// end init
}
}

View File

@ -29,12 +29,12 @@ extension DiscoveryPostsViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.statusFetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.statusFetchedResultsController.setRecords(
viewModel.statusFetchedResultsController.records.filter { $0.id != status.id }
viewModel.dataController.setRecords(
viewModel.dataController.records.filter { $0.id != status.id }
)
}

View File

@ -29,7 +29,7 @@ extension DiscoveryPostsViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -143,7 +143,7 @@ extension DiscoveryPostsViewModel.State {
self.offset = newOffset
var hasNewStatusesAppend = false
var statusIDs = isReloading ? [] : await viewModel.statusFetchedResultsController.records
var statusIDs = isReloading ? [] : await viewModel.dataController.records
for status in response.value {
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
statusIDs.append(.fromEntity(status))
@ -155,7 +155,7 @@ extension DiscoveryPostsViewModel.State {
} else {
await enter(state: NoMore.self)
}
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
await viewModel.dataController.setRecords(statusIDs)
viewModel.didLoadLatest.send()
} catch {

View File

@ -20,7 +20,7 @@ final class DiscoveryPostsViewModel {
// input
let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -45,8 +45,8 @@ final class DiscoveryPostsViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController()
// end init
self.dataController = StatusDataController()
// end init
Task {
await checkServerEndpoint()

View File

@ -29,11 +29,11 @@ extension HashtagTimelineViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.fetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.fetchedResultsController.deleteRecord(status)
viewModel.dataController.deleteRecord(status)
}
@MainActor

View File

@ -34,7 +34,7 @@ extension HashtagTimelineViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
fetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -146,7 +146,7 @@ extension HashtagTimelineViewModel.State {
self.maxID = newMaxID
var hasNewStatusesAppend = false
var statusIDs = isReloading ? [] : await viewModel.fetchedResultsController.records.map { $0.entity }
var statusIDs = isReloading ? [] : await viewModel.dataController.records.map { $0.entity }
for status in response.value {
guard !statusIDs.contains(status) else { continue }
statusIDs.append(status)
@ -159,7 +159,7 @@ extension HashtagTimelineViewModel.State {
await enter(state: NoMore.self)
}
await viewModel.fetchedResultsController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) })
await viewModel.dataController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) })
viewModel.didLoadLatest.send()
} catch {
await enter(state: Fail.self)

View File

@ -24,7 +24,7 @@ final class HashtagTimelineViewModel {
// input
let context: AppContext
let authContext: AuthContext
let fetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
let hashtagEntity = CurrentValueSubject<Mastodon.Entity.Tag?, Never>(nil)
@ -55,7 +55,7 @@ final class HashtagTimelineViewModel {
self.context = context
self.authContext = authContext
self.hashtag = hashtag
self.fetchedResultsController = StatusFetchedResultsController()
self.dataController = StatusDataController()
updateTagInformation()
// end init
}

View File

@ -34,11 +34,11 @@ extension HomeTimelineViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.fetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.fetchedResultsController.records = viewModel.fetchedResultsController.records.filter { $0.id != status.id }
viewModel.dataController.records = viewModel.dataController.records.filter { $0.id != status.id }
}
@MainActor

View File

@ -34,7 +34,7 @@ extension HomeTimelineViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
fetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -84,7 +84,7 @@ extension HomeTimelineViewModel.LoadLatestState {
guard let viewModel else { return }
let latestFeedRecords = viewModel.fetchedResultsController.records.prefix(APIService.onceRequestStatusMaxCount)
let latestFeedRecords = viewModel.dataController.records.prefix(APIService.onceRequestStatusMaxCount)
Task {
let latestStatusIDs: [Status.ID] = latestFeedRecords.compactMap { record in
@ -116,8 +116,8 @@ extension HomeTimelineViewModel.LoadLatestState {
var newRecords: [MastodonFeed] = newStatuses.map {
MastodonFeed.fromStatus(.fromEntity($0), kind: .home)
}
viewModel.fetchedResultsController.records = {
var oldRecords = viewModel.fetchedResultsController.records
viewModel.dataController.records = {
var oldRecords = viewModel.dataController.records
for (i, record) in newRecords.enumerated() {
if let index = oldRecords.firstIndex(where: { $0.status?.reblog?.id == record.id || $0.status?.id == record.id }) {
oldRecords[index] = record

View File

@ -32,7 +32,7 @@ extension HomeTimelineViewModel.LoadOldestState {
class Initial: HomeTimelineViewModel.LoadOldestState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard let viewModel = viewModel else { return false }
guard !viewModel.fetchedResultsController.records.isEmpty else { return false }
guard !viewModel.dataController.records.isEmpty else { return false }
return stateClass == Loading.self
}
}
@ -47,7 +47,7 @@ extension HomeTimelineViewModel.LoadOldestState {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let lastFeedRecord = viewModel.fetchedResultsController.records.last else {
guard let lastFeedRecord = viewModel.dataController.records.last else {
stateMachine.enter(Idle.self)
return
}

View File

@ -25,7 +25,7 @@ final class HomeTimelineViewModel: NSObject {
// input
let context: AppContext
let authContext: AuthContext
let fetchedResultsController: FeedFetchedResultsController
let dataController: FeedDataController
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -81,10 +81,10 @@ final class HomeTimelineViewModel: NSObject {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.fetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext)
self.dataController = FeedDataController(context: context, authContext: authContext)
self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context)
super.init()
self.fetchedResultsController.records = (try? FileManager.default.cachedHomeTimeline(for: authContext.mastodonAuthenticationBox).map {
self.dataController.records = (try? FileManager.default.cachedHomeTimeline(for: authContext.mastodonAuthenticationBox).map {
MastodonFeed.fromStatus($0, kind: .home)
}) ?? []
@ -103,7 +103,7 @@ final class HomeTimelineViewModel: NSObject {
}
.store(in: &disposeBag)
self.fetchedResultsController.$records
self.dataController.$records
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink(receiveValue: { feeds in
@ -115,7 +115,7 @@ final class HomeTimelineViewModel: NSObject {
})
.store(in: &disposeBag)
self.fetchedResultsController.loadInitial(kind: .home)
self.dataController.loadInitial(kind: .home)
}
}
@ -129,7 +129,7 @@ extension HomeTimelineViewModel {
extension HomeTimelineViewModel {
func timelineDidReachEnd() {
fetchedResultsController.loadNext(kind: .home)
dataController.loadNext(kind: .home)
}
}

View File

@ -38,11 +38,11 @@ extension NotificationTimelineViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.feedFetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.feedFetchedResultsController.delete(status: status)
viewModel.dataController.delete(status: status)
}
@MainActor

View File

@ -30,7 +30,7 @@ extension NotificationTimelineViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
feedFetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -32,7 +32,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
class Initial: NotificationTimelineViewModel.LoadOldestState {
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
guard let viewModel = viewModel else { return false }
guard !viewModel.feedFetchedResultsController.records.isEmpty else { return false }
guard !viewModel.dataController.records.isEmpty else { return false }
return stateClass == Loading.self
}
}
@ -47,7 +47,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
guard let lastFeedRecord = viewModel.feedFetchedResultsController.records.last else {
guard let lastFeedRecord = viewModel.dataController.records.last else {
stateMachine.enter(Fail.self)
return
}

View File

@ -20,7 +20,7 @@ final class NotificationTimelineViewModel {
let context: AppContext
let authContext: AuthContext
let scope: Scope
let feedFetchedResultsController: FeedFetchedResultsController
let dataController: FeedDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var isLoadingLatest = false
@Published var lastAutomaticFetchTimestamp: Date?
@ -52,20 +52,20 @@ final class NotificationTimelineViewModel {
self.context = context
self.authContext = authContext
self.scope = scope
self.feedFetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext)
self.dataController = FeedDataController(context: context, authContext: authContext)
switch scope {
case .everything:
self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox))?.map({ notification in
self.dataController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox))?.map({ notification in
MastodonFeed.fromNotification(notification, kind: .notificationAll)
}) ?? []
case .mentions:
self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox))?.map({ notification in
self.dataController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox))?.map({ notification in
MastodonFeed.fromNotification(notification, kind: .notificationMentions)
}) ?? []
}
self.feedFetchedResultsController.$records
self.dataController.$records
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink(receiveValue: { feeds in
@ -101,9 +101,9 @@ extension NotificationTimelineViewModel {
switch scope {
case .everything:
feedFetchedResultsController.loadInitial(kind: .notificationAll)
dataController.loadInitial(kind: .notificationAll)
case .mentions:
feedFetchedResultsController.loadInitial(kind: .notificationMentions)
dataController.loadInitial(kind: .notificationMentions)
}
didLoadLatest.send()
@ -113,9 +113,9 @@ extension NotificationTimelineViewModel {
func loadMore(item: NotificationItem) async {
switch scope {
case .everything:
feedFetchedResultsController.loadNext(kind: .notificationAll)
dataController.loadNext(kind: .notificationAll)
case .mentions:
feedFetchedResultsController.loadNext(kind: .notificationMentions)
dataController.loadNext(kind: .notificationMentions)
}
}
}

View File

@ -29,12 +29,12 @@ extension BookmarkViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.statusFetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.statusFetchedResultsController.setRecords(
viewModel.statusFetchedResultsController.records.filter { $0.id != status.id }
viewModel.dataController.setRecords(
viewModel.dataController.records.filter { $0.id != status.id }
)
}

View File

@ -32,7 +32,7 @@ extension BookmarkViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -58,7 +58,7 @@ extension BookmarkViewModel.State {
// reset
DispatchQueue.main.async {
viewModel.statusFetchedResultsController.reset()
viewModel.dataController.reset()
}
stateMachine.enter(Loading.self)
@ -130,7 +130,7 @@ extension BookmarkViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = await viewModel.statusFetchedResultsController.records.map { $0.entity }
var statusIDs = await viewModel.dataController.records.map { $0.entity }
for status in response.value {
guard !statusIDs.contains(status) else { continue }
statusIDs.append(status)
@ -150,7 +150,7 @@ extension BookmarkViewModel.State {
await enter(state: NoMore.self)
}
await viewModel.statusFetchedResultsController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) })
await viewModel.dataController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) })
} catch {
await enter(state: Fail.self)

View File

@ -20,7 +20,7 @@ final class BookmarkViewModel {
let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -42,7 +42,7 @@ final class BookmarkViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController()
self.dataController = StatusDataController()
}
}

View File

@ -29,12 +29,12 @@ extension FavoriteViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.statusFetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.statusFetchedResultsController.setRecords(
viewModel.statusFetchedResultsController.records.filter { $0.id != status.id }
viewModel.dataController.setRecords(
viewModel.dataController.records.filter { $0.id != status.id }
)
}

View File

@ -32,7 +32,7 @@ extension FavoriteViewModel {
stateMachine.enter(State.Reloading.self)
statusFetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -57,8 +57,8 @@ extension FavoriteViewModel.State {
Task {
// reset
await viewModel.statusFetchedResultsController.reset()
await viewModel.dataController.reset()
stateMachine.enter(Loading.self)
}
}
@ -129,7 +129,7 @@ extension FavoriteViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = await viewModel.statusFetchedResultsController.records
var statusIDs = await viewModel.dataController.records
for status in response.value {
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
statusIDs.append(.fromEntity(status))
@ -148,7 +148,7 @@ extension FavoriteViewModel.State {
} else {
await enter(state: NoMore.self)
}
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
await viewModel.dataController.setRecords(statusIDs)
} catch {
await enter(state: Fail.self)
}

View File

@ -19,7 +19,7 @@ final class FavoriteViewModel {
// input
let context: AppContext
let authContext: AuthContext
let statusFetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
// output
@ -41,7 +41,7 @@ final class FavoriteViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
self.statusFetchedResultsController = StatusFetchedResultsController()
self.dataController = StatusDataController()
}
}

View File

@ -943,9 +943,6 @@ extension ProfileViewController: MastodonMenuDelegate {
.followUser(_):
break
}
Task {
}
}
}

View File

@ -29,11 +29,11 @@ extension UserTimelineViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.statusFetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.statusFetchedResultsController.deleteRecord(status)
viewModel.dataController.deleteRecord(status)
}
@MainActor

View File

@ -49,7 +49,7 @@ extension UserTimelineViewModel {
).map { $0 || $1 || $2 || $3 }
Publishers.CombineLatest(
statusFetchedResultsController.$records,
dataController.$records,
needsTimelineHidden.removeDuplicates()
)
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)

View File

@ -57,7 +57,7 @@ extension UserTimelineViewModel.State {
Task {
// reset
await viewModel.statusFetchedResultsController.reset()
await viewModel.dataController.reset()
stateMachine.enter(Loading.self)
}
@ -116,8 +116,8 @@ extension UserTimelineViewModel.State {
Task {
let maxID = await viewModel.statusFetchedResultsController.records.last?.id
let maxID = await viewModel.dataController.records.last?.id
guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self)
return
@ -137,7 +137,7 @@ extension UserTimelineViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = await viewModel.statusFetchedResultsController.records
var statusIDs = await viewModel.dataController.records
for status in response.value {
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
statusIDs.append(.fromEntity(status))
@ -149,7 +149,7 @@ extension UserTimelineViewModel.State {
} else {
await enter(state: NoMore.self)
}
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
await viewModel.dataController.setRecords(statusIDs)
} catch {
await enter(state: Fail.self)

View File

@ -21,7 +21,7 @@ final class UserTimelineViewModel {
let context: AppContext
let authContext: AuthContext
let title: String
let statusFetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var userIdentifier: UserIdentifier?
@Published var queryFilter: QueryFilter
@ -59,7 +59,7 @@ final class UserTimelineViewModel {
self.context = context
self.authContext = authContext
self.title = title
self.statusFetchedResultsController = StatusFetchedResultsController()
self.dataController = StatusDataController()
self.queryFilter = queryFilter
}
}

View File

@ -32,7 +32,7 @@ extension ReportStatusViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
statusFetchedResultsController.$records
dataController.$records
.receive(on: DispatchQueue.main)
.sink { [weak self] records in
guard let self = self else { return }

View File

@ -66,7 +66,7 @@ extension ReportStatusViewModel.State {
Task {
let maxID = await viewModel.statusFetchedResultsController.records.last?.id
let maxID = await viewModel.dataController.records.last?.id
do {
let response = try await viewModel.context.apiService.userTimeline(
@ -80,7 +80,7 @@ extension ReportStatusViewModel.State {
)
var hasNewStatusesAppend = false
var statusIDs = await viewModel.statusFetchedResultsController.records
var statusIDs = await viewModel.dataController.records
for status in response.value {
guard !statusIDs.contains(where: { $0.id == status.id }) else { continue }
statusIDs.append(.fromEntity(status))
@ -92,7 +92,7 @@ extension ReportStatusViewModel.State {
} else {
await enter(state: NoMore.self)
}
await viewModel.statusFetchedResultsController.setRecords(statusIDs)
await viewModel.dataController.setRecords(statusIDs)
} catch {
await enter(state: Fail.self)

View File

@ -26,7 +26,7 @@ class ReportStatusViewModel {
let authContext: AuthContext
let account: Mastodon.Entity.Account
let status: MastodonStatus?
let statusFetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var isSkip = false
@ -59,7 +59,7 @@ class ReportStatusViewModel {
self.authContext = authContext
self.account = account
self.status = status
self.statusFetchedResultsController = StatusFetchedResultsController()
self.dataController = StatusDataController()
// end init
if let status = status {

View File

@ -88,7 +88,7 @@ class MainTabBarController: UITabBarController {
}
@MainActor
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) async -> UIViewController {
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController {
guard let authContext, let me = authContext.mastodonAuthenticationBox.authentication.account() else {
return UITableViewController()
}
@ -170,39 +170,38 @@ extension MainTabBarController {
view.backgroundColor = .systemBackground
// seealso: `ThemeService.apply(theme:)`
Task { @MainActor in
let tabs = Tab.allCases
var viewControllers = [UIViewController]()
for tab in tabs {
let viewController = await tab.viewController(context: context, authContext: authContext, coordinator: coordinator)
viewController.tabBarItem.tag = tab.tag
viewController.tabBarItem.title = tab.title // needs for acessiblity large content label
viewController.tabBarItem.image = tab.image.imageWithoutBaseline()
viewController.tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
viewController.tabBarItem.accessibilityLabel = tab.title
viewController.tabBarItem.accessibilityUserInputLabels = tab.inputLabels
viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
viewControllers.append(viewController)
let tabs = Tab.allCases
var viewControllers = [UIViewController]()
for tab in tabs {
let viewController = tab.viewController(context: context, authContext: authContext, coordinator: coordinator)
viewController.tabBarItem.tag = tab.tag
viewController.tabBarItem.title = tab.title // needs for acessiblity large content label
viewController.tabBarItem.image = tab.image.imageWithoutBaseline()
viewController.tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
viewController.tabBarItem.accessibilityLabel = tab.title
viewController.tabBarItem.accessibilityUserInputLabels = tab.inputLabels
viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
viewControllers.append(viewController)
}
_viewControllers = viewControllers
setViewControllers(viewControllers, animated: false)
selectedIndex = 0
// hacky workaround for FB11986255 (Setting accessibilityUserInputLabels on a UITabBarItem has no effect)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
if let searchItem = self.tabBar.subviews.first(where: { $0.accessibilityLabel == Tab.search.title }) {
searchItem.accessibilityUserInputLabels = Tab.search.inputLabels
}
_viewControllers = viewControllers
setViewControllers(viewControllers, animated: false)
selectedIndex = 0
// hacky workaround for FB11986255 (Setting accessibilityUserInputLabels on a UITabBarItem has no effect)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
if let searchItem = self.tabBar.subviews.first(where: { $0.accessibilityLabel == Tab.search.title }) {
searchItem.accessibilityUserInputLabels = Tab.search.inputLabels
}
}
context.apiService.error
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
guard let self = self, let coordinator = self.coordinator else { return }
switch error {
}
context.apiService.error
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
guard let self = self, let coordinator = self.coordinator else { return }
switch error {
case .implicit:
break
case .explicit:
@ -214,99 +213,98 @@ extension MainTabBarController {
from: nil,
transition: .alertController(animated: true, completion: nil)
)
}
}
.store(in: &disposeBag)
// handle post failure
// handle push notification.
// toggle entry when finish fetch latest notification
Publishers.CombineLatest(
context.notificationService.unreadNotificationCountDidUpdate,
$currentTab
)
.receive(on: DispatchQueue.main)
.sink { [weak self] authentication, currentTab in
guard let self = self else { return }
guard let notificationViewController = self.notificationViewController else { return }
let authentication = self.authContext?.mastodonAuthenticationBox.userAuthorization
let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.accessToken)
return count > 0
} ?? false
let image: UIImage
if hasUnreadPushNotification {
let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor])
image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)!
} else {
image = Tab.notifications.image
}
notificationViewController.tabBarItem.image = image.imageWithoutBaseline()
notificationViewController.navigationController?.tabBarItem.image = image.imageWithoutBaseline()
}
.store(in: &disposeBag)
layoutAvatarButton()
$avatarURL
.receive(on: DispatchQueue.main)
.sink { [weak self] avatarURL in
guard let self = self else { return }
self.avatarButton.avatarImageView.setImage(
url: avatarURL,
placeholder: .placeholder(color: .systemFill),
scaleToSize: MainTabBarController.avatarButtonSize
)
}
.store(in: &disposeBag)
NotificationCenter.default.publisher(for: .userFetched)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else { return }
if let account = self.authContext?.mastodonAuthenticationBox.authentication.account() {
self.avatarURL = account.avatarImageURL()
// a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
guard let profileTabItem = _profileTabItem else { return }
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(account.displayNameWithFallback)
self.context.authenticationService.updateActiveUserAccountPublisher
.sink { [weak self] in
self?.updateUserAccount()
}
.store(in: &self.disposeBag)
}
}
.store(in: &disposeBag)
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
tabBarLongPressGestureRecognizer.delegate = self
tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer)
let tabBarDoubleTapGestureRecognizer = UITapGestureRecognizer()
tabBarDoubleTapGestureRecognizer.numberOfTapsRequired = 2
tabBarDoubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarDoubleTapGestureRecognizerHandler(_:)))
tabBarDoubleTapGestureRecognizer.delaysTouchesEnded = false
tabBar.addGestureRecognizer(tabBarDoubleTapGestureRecognizer)
self.isReadyForWizardAvatarButton = authContext != nil
// handle post failure
// handle push notification.
// toggle entry when finish fetch latest notification
Publishers.CombineLatest(
context.notificationService.unreadNotificationCountDidUpdate,
$currentTab
.receive(on: DispatchQueue.main)
.sink { [weak self] tab in
guard let self = self else { return }
self.updateAvatarButtonAppearance()
}
.store(in: &disposeBag)
)
.receive(on: DispatchQueue.main)
.sink { [weak self] authentication, currentTab in
guard let self = self else { return }
guard let notificationViewController = self.notificationViewController else { return }
updateTabBarDisplay()
let authentication = self.authContext?.mastodonAuthenticationBox.userAuthorization
let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.accessToken)
return count > 0
} ?? false
let image: UIImage
if hasUnreadPushNotification {
let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor])
image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)!
} else {
image = Tab.notifications.image
}
notificationViewController.tabBarItem.image = image.imageWithoutBaseline()
notificationViewController.navigationController?.tabBarItem.image = image.imageWithoutBaseline()
}
.store(in: &disposeBag)
layoutAvatarButton()
$avatarURL
.receive(on: DispatchQueue.main)
.sink { [weak self] avatarURL in
guard let self = self else { return }
self.avatarButton.avatarImageView.setImage(
url: avatarURL,
placeholder: .placeholder(color: .systemFill),
scaleToSize: MainTabBarController.avatarButtonSize
)
}
.store(in: &disposeBag)
NotificationCenter.default.publisher(for: .userFetched)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
guard let self else { return }
if let account = self.authContext?.mastodonAuthenticationBox.authentication.account() {
self.avatarURL = account.avatarImageURL()
// a11y
let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag }
guard let profileTabItem = _profileTabItem else { return }
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(account.displayNameWithFallback)
self.context.authenticationService.updateActiveUserAccountPublisher
.sink { [weak self] in
self?.updateUserAccount()
}
.store(in: &self.disposeBag)
}
}
.store(in: &disposeBag)
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
tabBarLongPressGestureRecognizer.delegate = self
tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer)
let tabBarDoubleTapGestureRecognizer = UITapGestureRecognizer()
tabBarDoubleTapGestureRecognizer.numberOfTapsRequired = 2
tabBarDoubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarDoubleTapGestureRecognizerHandler(_:)))
tabBarDoubleTapGestureRecognizer.delaysTouchesEnded = false
tabBar.addGestureRecognizer(tabBarDoubleTapGestureRecognizer)
self.isReadyForWizardAvatarButton = authContext != nil
$currentTab
.receive(on: DispatchQueue.main)
.sink { [weak self] tab in
guard let self = self else { return }
self.updateAvatarButtonAppearance()
}
.store(in: &disposeBag)
updateTabBarDisplay()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

View File

@ -34,11 +34,11 @@ extension SearchResultViewController: DataSourceProvider {
}
func update(status: MastodonStatus) {
viewModel.statusFetchedResultsController.update(status: status)
viewModel.dataController.update(status: status)
}
func delete(status: MastodonStatus) {
viewModel.statusFetchedResultsController.deleteRecord(status)
viewModel.dataController.deleteRecord(status)
}
@MainActor

View File

@ -33,7 +33,7 @@ extension SearchResultViewModel {
diffableDataSource.apply(snapshot, animatingDifferences: false)
Publishers.CombineLatest3(
statusFetchedResultsController.$records,
dataController.$records,
$accounts,
$hashtags
)

View File

@ -147,20 +147,21 @@ extension SearchResultViewModel.State {
// reset data source when the search is refresh
if offset == nil {
await viewModel.statusFetchedResultsController.reset()
viewModel.relationships = []
viewModel.accounts = []
await viewModel.dataController.reset()
viewModel.hashtags = []
}
await viewModel.statusFetchedResultsController.appendRecords(statuses)
// due to combine relationships must be set first
var existingRelationships = viewModel.relationships
for hashtag in relationships where !existingRelationships.contains(hashtag) {
existingRelationships.append(hashtag)
}
viewModel.relationships = existingRelationships
await viewModel.dataController.appendRecords(statuses)
var existingHashtags = viewModel.hashtags
for hashtag in searchResults.hashtags where !existingHashtags.contains(hashtag) {

View File

@ -24,7 +24,7 @@ final class SearchResultViewModel {
@Published var hashtags: [Mastodon.Entity.Tag] = []
@Published var accounts: [Mastodon.Entity.Account] = []
var relationships: [Mastodon.Entity.Relationship] = []
let statusFetchedResultsController: StatusFetchedResultsController
let dataController: StatusDataController
let listBatchFetchViewModel = ListBatchFetchViewModel()
var cellFrameCache = NSCache<NSNumber, NSValue>()
@ -55,6 +55,6 @@ final class SearchResultViewModel {
self.accounts = []
self.relationships = []
self.statusFetchedResultsController = StatusFetchedResultsController()
self.dataController = StatusDataController()
}
}

View File

@ -77,7 +77,6 @@ final class MastodonStatusThreadViewModel {
extension MastodonStatusThreadViewModel {
func appendAncestor(
domain: String,
nodes: [Node]
) {
var newItems: [StatusItem] = []
@ -91,7 +90,6 @@ extension MastodonStatusThreadViewModel {
}
func appendDescendant(
domain: String,
nodes: [Node]
) {

View File

@ -74,7 +74,6 @@ extension ThreadViewModel.LoadThreadState {
authenticationBox: viewModel.authContext.mastodonAuthenticationBox)
viewModel.mastodonStatusThreadViewModel.appendAncestor(
domain: threadContext.domain,
nodes: MastodonStatusThreadViewModel.Node.replyToThread(
for: threadContext.replyToID,
from: response.value.ancestors
@ -82,7 +81,6 @@ extension ThreadViewModel.LoadThreadState {
)
viewModel.mastodonStatusThreadViewModel.appendDescendant(
domain: threadContext.domain,
nodes: response.value.descendants.map { status in
return .init(status: .fromEntity(status), children: [])
}

View File

@ -68,7 +68,6 @@ class ThreadViewModel {
// bind threadContext
self.threadContext = .init(
domain: authContext.mastodonAuthenticationBox.domain, //status.domain,
statusID: status.id,
replyToID: status.entity.inReplyToID
)
@ -111,7 +110,6 @@ class ThreadViewModel {
extension ThreadViewModel {
struct ThreadContext {
let domain: String
let statusID: Mastodon.Entity.Status.ID
let replyToID: Mastodon.Entity.Status.ID?
}

View File

@ -0,0 +1,90 @@
import Foundation
import UIKit
import Combine
import MastodonSDK
final public class FeedDataController {
@Published public var records: [MastodonFeed] = []
private let context: AppContext
private let authContext: AuthContext
public init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
}
public func loadInitial(kind: MastodonFeed.Kind) {
Task {
records = try await load(kind: kind, sinceId: nil)
}
}
public func loadNext(kind: MastodonFeed.Kind) {
Task {
guard let lastId = records.last?.status?.id else {
return loadInitial(kind: kind)
}
records = try await load(kind: kind, sinceId: lastId)
}
}
public func update(status: MastodonStatus) {
var newRecords = Array(records)
for (i, record) in newRecords.enumerated() {
if record.status?.id == status.id {
newRecords[i] = .fromStatus(status, kind: record.kind)
} else if let reblog = status.reblog, reblog.id == record.status?.id {
newRecords[i] = .fromStatus(status, kind: record.kind)
} else if let reblog = record.status?.reblog, reblog.id == status.id {
// Handle reblogged state
let isRebloggedByAnyOne: Bool = records[i].status!.reblog != nil
let newStatus: MastodonStatus
if isRebloggedByAnyOne {
// if status was previously reblogged by me: remove reblogged status
if records[i].status!.entity.reblogged == true && status.entity.reblogged == false {
newStatus = .fromEntity(status.entity)
} else {
newStatus = .fromEntity(records[i].status!.entity)
}
} else {
newStatus = .fromEntity(status.entity)
}
newStatus.isSensitiveToggled = status.isSensitiveToggled
newStatus.reblog = isRebloggedByAnyOne ? .fromEntity(status.entity) : nil
newRecords[i] = .fromStatus(newStatus, kind: record.kind)
} else if let reblog = record.status?.reblog, reblog.id == status.reblog?.id {
// Handle re-reblogged state
newRecords[i] = .fromStatus(status, kind: record.kind)
}
}
records = newRecords
}
public func delete(status: MastodonStatus) {
self.records.removeAll { $0.id == status.id }
}
}
private extension FeedDataController {
func load(kind: MastodonFeed.Kind, sinceId: MastodonStatus.ID?) async throws -> [MastodonFeed] {
switch kind {
case .home:
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
case .notificationAll:
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationAll) }
case .notificationMentions:
return try await context.apiService.notifications(maxID: nil, scope: .mentions, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationMentions) }
}
}
}

View File

@ -1,95 +0,0 @@
//
// FeedFetchedResultsController.swift
// FeedFetchedResultsController
//
// Created by Cirno MainasuK on 2021-8-19.
// Copyright © 2021 Twidere. All rights reserved.
//
import Foundation
import UIKit
import Combine
import MastodonSDK
final public class FeedFetchedResultsController {
@Published public var records: [MastodonFeed] = []
private let context: AppContext
private let authContext: AuthContext
public init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
}
public func loadInitial(kind: MastodonFeed.Kind) {
Task {
records = try await load(kind: kind, sinceId: nil)
}
}
public func loadNext(kind: MastodonFeed.Kind) {
Task {
guard let lastId = records.last?.status?.id else {
return loadInitial(kind: kind)
}
records = try await load(kind: kind, sinceId: lastId)
}
}
public func update(status: MastodonStatus) {
var newRecords = Array(records)
for (i, record) in newRecords.enumerated() {
if record.status?.id == status.id {
newRecords[i] = .fromStatus(status, kind: record.kind)
} else if let reblog = status.reblog, reblog.id == record.status?.id {
newRecords[i] = .fromStatus(status, kind: record.kind)
} else if let reblog = record.status?.reblog, reblog.id == status.id {
// Handle reblogged state
switch status.entity.reblogged {
case .some(true):
newRecords[i] = .fromStatus({
let stat = MastodonStatus.fromEntity(records[i].status!.entity)
stat.isSensitiveToggled = status.isSensitiveToggled
stat.reblog = .fromEntity(status.entity)
return stat
}(), kind: record.kind)
case .some(false), .none:
newRecords[i] = .fromStatus({
let stat = MastodonStatus.fromEntity(status.entity)
stat.isSensitiveToggled = status.isSensitiveToggled
return stat
}(), kind: record.kind)
}
} else if let reblog = record.status?.reblog, reblog.id == status.reblog?.id {
// Handle re-reblogged state
newRecords[i] = .fromStatus(status, kind: record.kind)
}
}
records = newRecords
}
public func delete(status: MastodonStatus) {
self.records.removeAll { $0.id == status.id }
}
}
private extension FeedFetchedResultsController {
func load(kind: MastodonFeed.Kind, sinceId: MastodonStatus.ID?) async throws -> [MastodonFeed] {
switch kind {
case .home:
await context.authenticationService.authenticationServiceProvider.fetchAccounts(apiService: context.apiService)
return try await context.apiService.homeTimeline(sinceID: sinceId, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromStatus(.fromEntity($0), kind: .home) }
case .notificationAll:
return try await context.apiService.notifications(maxID: nil, scope: .everything, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationAll) }
case .notificationMentions:
return try await context.apiService.notifications(maxID: nil, scope: .mentions, authenticationBox: authContext.mastodonAuthenticationBox)
.value.map { .fromNotification($0, kind: .notificationMentions) }
}
}
}

View File

@ -1,17 +1,10 @@
//
// StatusFetchedResultsController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-30.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
public final class StatusFetchedResultsController {
public final class StatusDataController {
@MainActor
@Published
public private(set) var records: [MastodonStatus] = []
@ -51,22 +44,25 @@ public final class StatusFetchedResultsController {
newRecords[i] = status
} else if let reblog = record.reblog, reblog.id == status.id {
// Handle reblogged state
switch status.entity.reblogged {
case .some(true):
newRecords[i] = {
let stat = MastodonStatus.fromEntity(records[i].entity)
stat.isSensitiveToggled = status.isSensitiveToggled
stat.reblog = .fromEntity(status.entity)
return stat
}()
case .some(false), .none:
newRecords[i] = {
let stat = MastodonStatus.fromEntity(status.entity)
stat.isSensitiveToggled = status.isSensitiveToggled
return stat
}()
let isRebloggedByAnyOne: Bool = records[i].reblog != nil
let newStatus: MastodonStatus
if isRebloggedByAnyOne {
// if status was previously reblogged by me: remove reblogged status
if records[i].entity.reblogged == true && status.entity.reblogged == false {
newStatus = .fromEntity(status.entity)
} else {
newStatus = .fromEntity(records[i].entity)
}
} else {
newStatus = .fromEntity(status.entity)
}
newStatus.isSensitiveToggled = status.isSensitiveToggled
newStatus.reblog = isRebloggedByAnyOne ? .fromEntity(status.entity) : nil
newRecords[i] = newStatus
} else if let reblog = record.reblog, reblog.id == status.reblog?.id {
// Handle re-reblogged state
newRecords[i] = status

View File

@ -58,3 +58,15 @@ public extension Mastodon.Entity.Status {
return MastodonVisibility(rawValue: visibility)
}
}
public extension MastodonStatus {
func getPoll(in context: NSManagedObjectContext, domain: String) async -> Poll? {
guard
let pollId = entity.poll?.id
else { return nil }
return try? await context.perform {
let predicate = Poll.predicate(domain: domain, id: pollId)
return Poll.findOrFetch(in: context, matching: predicate)
}
}
}

View File

@ -107,7 +107,7 @@ extension StatusView {
} else if status.reblog != nil {
let name = status.entity.account.displayNameWithFallback
let emojis = status.entity.account.emojis
viewModel.header = {
let text = L10n.Common.Controls.Status.userReblogged(name)
let content = MastodonContent(content: text, emojis: emojis.asDictionary)
@ -201,7 +201,7 @@ extension StatusView {
// author avatar
viewModel.authorAvatarImageURL = author.avatarImageURL()
let emojis = author.emojis.asDictionary
// author name
viewModel.authorName = {
do {
@ -530,15 +530,3 @@ extension StatusView {
}
}
extension MastodonStatus {
func getPoll(in context: NSManagedObjectContext, domain: String) async -> Poll? {
guard
let pollId = entity.poll?.id
else { return nil }
return try? await context.perform {
let predicate = Poll.predicate(domain: domain, id: pollId)
return Poll.findOrFetch(in: context, matching: predicate)
}
}
}