mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
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:
commit
febbc6f22a
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -943,9 +943,6 @@ extension ProfileViewController: MastodonMenuDelegate {
|
||||
.followUser(_):
|
||||
break
|
||||
}
|
||||
|
||||
Task {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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?) {
|
||||
|
@ -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
|
||||
|
@ -33,7 +33,7 @@ extension SearchResultViewModel {
|
||||
diffableDataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
Publishers.CombineLatest3(
|
||||
statusFetchedResultsController.$records,
|
||||
dataController.$records,
|
||||
$accounts,
|
||||
$hashtags
|
||||
)
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
) {
|
||||
|
||||
|
@ -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: [])
|
||||
}
|
||||
|
@ -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?
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user