From b010b6112eab36665a917739eee8337e138f5089 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 22 Nov 2023 12:32:04 +0100 Subject: [PATCH 001/159] Remove usage of Status (IOS-176) --- .../Notification/NotificationItem.swift | 6 +- .../Notification/NotificationSection.swift | 19 +- Mastodon/Diffable/Report/ReportItem.swift | 3 +- Mastodon/Diffable/Report/ReportSection.swift | 19 +- Mastodon/Diffable/Status/StatusItem.swift | 13 +- Mastodon/Diffable/Status/StatusSection.swift | 89 ++-- .../Provider/DataSourceFacade+Bookmark.swift | 3 +- .../Provider/DataSourceFacade+Favorite.swift | 6 +- .../Provider/DataSourceFacade+Follow.swift | 71 +-- .../Provider/DataSourceFacade+Media.swift | 10 +- .../Provider/DataSourceFacade+Meta.swift | 9 +- .../Provider/DataSourceFacade+Model.swift | 49 +-- .../Provider/DataSourceFacade+Profile.swift | 11 +- .../Provider/DataSourceFacade+Reblog.swift | 6 +- .../Provider/DataSourceFacade+Status.swift | 54 +-- .../Provider/DataSourceFacade+Thread.swift | 7 +- .../Provider/DataSourceFacade+Translate.swift | 18 +- .../Provider/DataSourceFacade+URL.swift | 3 +- ...er+NotificationTableViewCellDelegate.swift | 77 +--- ...Provider+StatusTableViewCellDelegate.swift | 23 +- ...tatusTableViewControllerNavigateable.swift | 10 +- ...taSourceProvider+UITableViewDelegate.swift | 12 +- .../Provider/DataSourceProvider.swift | 4 +- Mastodon/Scene/Compose/ComposeViewModel.swift | 2 +- .../DiscoveryCommunityViewModel+State.swift | 8 +- .../DiscoveryCommunityViewModel.swift | 7 +- .../Scene/Discovery/DiscoveryViewModel.swift | 1 + .../Posts/DiscoveryPostsViewModel+State.swift | 8 +- .../Posts/DiscoveryPostsViewModel.swift | 7 +- .../HashtagTimelineViewModel+State.swift | 9 +- .../HashtagTimelineViewModel.swift | 7 +- ...ineViewController+DataSourceProvider.swift | 9 +- .../HomeTimelineViewModel+Diffable.swift | 62 +-- ...omeTimelineViewModel+LoadLatestState.swift | 6 +- ...omeTimelineViewModel+LoadOldestState.swift | 9 +- .../HomeTimeline/HomeTimelineViewModel.swift | 70 ++- .../NotificationTableViewCell+ViewModel.swift | 3 +- ...ineViewController+DataSourceProvider.swift | 12 +- .../NotificationTimelineViewController.swift | 36 +- ...tificationTimelineViewModel+Diffable.swift | 61 +-- ...ionTimelineViewModel+LoadOldestState.swift | 7 +- .../NotificationTimelineViewModel.swift | 38 +- .../Bookmark/BookmarkViewModel+State.swift | 14 +- .../Profile/Bookmark/BookmarkViewModel.swift | 7 +- .../Profile/CachedProfileViewModel.swift | 1 + .../Favorite/FavoriteViewModel+State.swift | 18 +- .../Profile/Favorite/FavoriteViewModel.swift | 7 +- .../Scene/Profile/MeProfileViewModel.swift | 1 + Mastodon/Scene/Profile/ProfileViewModel.swift | 1 + .../Profile/RemoteProfileViewModel.swift | 3 + .../UserTimelineViewModel+State.swift | 36 +- .../Timeline/UserTimelineViewModel.swift | 7 +- .../Profile/UserLIst/UserListViewModel.swift | 5 +- .../Scene/Report/Report/ReportViewModel.swift | 19 +- .../ReportStatusViewModel+State.swift | 11 +- .../ReportStatus/ReportStatusViewModel.swift | 13 +- .../ReportStatusTableViewCell+ViewModel.swift | 6 +- .../Root/MainTab/MainTabBarController.swift | 36 +- .../SearchResultOverviewCoordinator.swift | 14 +- .../SearchResult/SearchResultItem.swift | 2 +- .../SearchResult/SearchResultSection.swift | 19 +- .../SearchResultViewModel+State.swift | 6 +- .../SearchResult/SearchResultViewModel.swift | 7 +- .../NotificationView+Configuration.swift | 45 +- .../StatusTableViewCell+ViewModel.swift | 14 +- ...tusThreadRootTableViewCell+ViewModel.swift | 4 +- .../Scene/Thread/CachedThreadViewModel.swift | 6 +- .../StatusEditHistoryTableViewCell.swift | 2 +- .../StatusEditHistoryViewModel.swift | 2 +- .../MastodonStatusThreadViewModel.swift | 93 ++-- .../Scene/Thread/RemoteThreadViewModel.swift | 22 +- .../Thread/ThreadViewModel+Diffable.swift | 6 +- Mastodon/Scene/Thread/ThreadViewModel.swift | 40 +- .../FeedFetchedResultsController.swift | 84 +--- .../StatusFetchedResultsController.swift | 100 +---- .../Service/API/APIService+Bookmark.swift | 44 +- .../Service/API/APIService+Favorite.swift | 77 +--- .../Service/API/APIService+Reblog.swift | 67 +-- .../Service/API/APIService+Status.swift | 11 +- .../Entity/Mastodon+Entity+Account.swift | 9 + .../Entity/Mastodon+Entity+Card.swift | 10 + .../Entity/Mastodon+Entity+Notification.swift | 10 + .../Entity/Mastodon+Entity+Status.swift | 11 + .../Sources/MastodonSDK/MastodonFeed.swift | 56 +++ .../MastodonSDK/MastodonNotification.swift | 45 ++ .../Sources/MastodonSDK/MastodonStatus.swift | 54 +++ .../Protocol/StatusCompatible.swift | 1 + .../ComposeContentViewModel+DataSource.swift | 6 +- .../ComposeContentViewModel.swift | 91 ++-- .../Publisher/MastodonStatusPublisher.swift | 6 +- .../Content/MediaView+Configuration.swift | 8 +- .../Content/NotificationView+ViewModel.swift | 2 +- .../View/Content/StatusCardControl.swift | 25 +- .../Content/StatusView+Configuration.swift | 415 +++++++----------- .../View/Content/StatusView+ViewModel.swift | 7 +- .../MastodonUI/View/Content/StatusView.swift | 2 +- ...eMiddleLoaderTableViewCell+ViewModel.swift | 11 +- 97 files changed, 1048 insertions(+), 1455 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift diff --git a/Mastodon/Diffable/Notification/NotificationItem.swift b/Mastodon/Diffable/Notification/NotificationItem.swift index b0fdddb7f..d5727e813 100644 --- a/Mastodon/Diffable/Notification/NotificationItem.swift +++ b/Mastodon/Diffable/Notification/NotificationItem.swift @@ -7,10 +7,10 @@ import CoreData import Foundation -import CoreDataStack +import MastodonSDK enum NotificationItem: Hashable { - case feed(record: ManagedObjectRecord) - case feedLoader(record: ManagedObjectRecord) + case feed(record: MastodonFeed) + case feedLoader(record: MastodonFeed) case bottomLoader } diff --git a/Mastodon/Diffable/Notification/NotificationSection.swift b/Mastodon/Diffable/Notification/NotificationSection.swift index 0271aac20..0b446336f 100644 --- a/Mastodon/Diffable/Notification/NotificationSection.swift +++ b/Mastodon/Diffable/Notification/NotificationSection.swift @@ -41,18 +41,15 @@ extension NotificationSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { - case .feed(let record): + case .feed(let feed): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell - context.managedObjectContext.performAndWait { - guard let feed = record.object(in: context.managedObjectContext) else { return } - configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)), - configuration: configuration - ) - } + configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: NotificationTableViewCell.ViewModel(value: .feed(feed)), + configuration: configuration + ) return cell case .feedLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell diff --git a/Mastodon/Diffable/Report/ReportItem.swift b/Mastodon/Diffable/Report/ReportItem.swift index f5ea387b6..ed083f427 100644 --- a/Mastodon/Diffable/Report/ReportItem.swift +++ b/Mastodon/Diffable/Report/ReportItem.swift @@ -7,10 +7,11 @@ import Foundation import CoreDataStack +import MastodonSDK enum ReportItem: Hashable { case header(context: HeaderContext) - case status(record: ManagedObjectRecord) + case status(record: MastodonStatus) case comment(context: CommentContext) case result(record: ManagedObjectRecord) case bottomLoader diff --git a/Mastodon/Diffable/Report/ReportSection.swift b/Mastodon/Diffable/Report/ReportSection.swift index 99e04ea1f..94161f28c 100644 --- a/Mastodon/Diffable/Report/ReportSection.swift +++ b/Mastodon/Diffable/Report/ReportSection.swift @@ -45,18 +45,15 @@ extension ReportSection { cell.primaryLabel.text = headerContext.primaryLabelText cell.secondaryLabel.text = headerContext.secondaryLabelText return cell - case .status(let record): + case .status(let status): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { return } - configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: .init(value: status), - configuration: configuration - ) - } + configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: .init(value: status), + configuration: configuration + ) return cell case .comment(let commentContext): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportCommentTableViewCell.self), for: indexPath) as! ReportCommentTableViewCell diff --git a/Mastodon/Diffable/Status/StatusItem.swift b/Mastodon/Diffable/Status/StatusItem.swift index 1d08ea41d..938e51eb2 100644 --- a/Mastodon/Diffable/Status/StatusItem.swift +++ b/Mastodon/Diffable/Status/StatusItem.swift @@ -8,11 +8,12 @@ import Foundation import CoreDataStack import MastodonUI +import MastodonSDK enum StatusItem: Hashable { - case feed(record: ManagedObjectRecord) - case feedLoader(record: ManagedObjectRecord) - case status(record: ManagedObjectRecord) + case feed(record: MastodonFeed) + case feedLoader(record: MastodonFeed) + case status(record: MastodonStatus) case thread(Thread) case topLoader case bottomLoader @@ -24,7 +25,7 @@ extension StatusItem { case reply(context: Context) case leaf(context: Context) - public var record: ManagedObjectRecord { + public var record: MastodonStatus { switch self { case .root(let threadContext), .reply(let threadContext), @@ -37,12 +38,12 @@ extension StatusItem { extension StatusItem.Thread { class Context: Hashable { - let status: ManagedObjectRecord + let status: MastodonStatus var displayUpperConversationLink: Bool var displayBottomConversationLink: Bool init( - status: ManagedObjectRecord, + status: MastodonStatus, displayUpperConversationLink: Bool = false, displayBottomConversationLink: Bool = false ) { diff --git a/Mastodon/Diffable/Status/StatusSection.swift b/Mastodon/Diffable/Status/StatusSection.swift index 586764f42..d6dbeccf6 100644 --- a/Mastodon/Diffable/Status/StatusSection.swift +++ b/Mastodon/Diffable/Status/StatusSection.swift @@ -44,42 +44,33 @@ extension StatusSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in switch item { - case .feed(let record): + case .feed(let feed): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - context.managedObjectContext.performAndWait { - guard let feed = record.object(in: context.managedObjectContext) else { return } - configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)), - configuration: configuration - ) - } + configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)), + configuration: configuration + ) return cell - case .feedLoader(let record): + case .feedLoader(let feed): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell - context.managedObjectContext.performAndWait { - guard let feed = record.object(in: context.managedObjectContext) else { return } - configure( - cell: cell, - feed: feed, - configuration: configuration - ) - } + configure( + cell: cell, + feed: feed, + configuration: configuration + ) return cell - case .status(let record): + case .status(let status): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { return } - configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .status(status)), - configuration: configuration - ) - } + configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: StatusTableViewCell.ViewModel(value: .status(status)), + configuration: configuration + ) return cell case .thread(let thread): let cell = dequeueConfiguredReusableCell( @@ -124,30 +115,24 @@ extension StatusSection { switch configuration.thread { case .root(let threadContext): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell - managedObjectContext.performAndWait { - guard let status = threadContext.status.object(in: managedObjectContext) else { return } - StatusSection.configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: StatusThreadRootTableViewCell.ViewModel(value: .status(status)), - configuration: configuration.configuration - ) - } + StatusSection.configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: StatusThreadRootTableViewCell.ViewModel(value: .status(threadContext.status)), + configuration: configuration.configuration + ) return cell case .reply(let threadContext), .leaf(let threadContext): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - managedObjectContext.performAndWait { - guard let status = threadContext.status.object(in: managedObjectContext) else { return } - StatusSection.configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .status(status)), - configuration: configuration.configuration - ) - } + StatusSection.configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: StatusTableViewCell.ViewModel(value: .status(threadContext.status)), + configuration: configuration.configuration + ) return cell } } @@ -319,7 +304,7 @@ extension StatusSection { static func configure( cell: TimelineMiddleLoaderTableViewCell, - feed: Feed, + feed: MastodonFeed, configuration: Configuration ) { cell.configure( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 2c54653ba..70a3fdbc0 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -9,11 +9,12 @@ import UIKit import CoreData import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { public static func responseToStatusBookmarkAction( provider: UIViewController & NeedsDependency & AuthContextProvider, - status: ManagedObjectRecord + status: MastodonStatus ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift index 92945b9ee..8797ea986 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -7,19 +7,19 @@ import UIKit import CoreData -import CoreDataStack +import MastodonSDK import MastodonCore extension DataSourceFacade { public static func responseToStatusFavoriteAction( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord + status: MastodonStatus ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await provider.context.apiService.favorite( - record: status, + status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox ) } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 6fe0005a0..88bc41911 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -46,15 +46,14 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToUserFollowRequestAction( dependency: NeedsDependency & AuthContextProvider, - notification: ManagedObjectRecord, + notification: MastodonNotification, query: Mastodon.API.Account.FollowRequestQuery ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - + let managedObjectContext = dependency.context.managedObjectContext let _userID: MastodonUser.ID? = try await managedObjectContext.perform { - guard let notification = notification.object(in: managedObjectContext) else { return nil } return notification.account.id } @@ -63,23 +62,17 @@ extension DataSourceFacade { throw APIService.APIError.implicit(.badRequest) } - let state: MastodonFollowRequestState = try await managedObjectContext.perform { - guard let notification = notification.object(in: managedObjectContext) else { return .init(state: .none) } - return notification.followRequestState - } + let state: MastodonFollowRequestState = notification.followRequestState guard state.state == .none else { return } - try? await managedObjectContext.performChanges { - guard let notification = notification.object(in: managedObjectContext) else { return } - switch query { - case .accept: - notification.transientFollowRequestState = .init(state: .isAccepting) - case .reject: - notification.transientFollowRequestState = .init(state: .isRejecting) - } + switch query { + case .accept: + notification.transientFollowRequestState = .init(state: .isAccepting) + case .reject: + notification.transientFollowRequestState = .init(state: .isRejecting) } do { @@ -90,22 +83,12 @@ extension DataSourceFacade { ) } catch { // reset state when failure - try? await managedObjectContext.performChanges { - guard let notification = notification.object(in: managedObjectContext) else { return } - notification.transientFollowRequestState = .init(state: .none) - } - + notification.transientFollowRequestState = .init(state: .none) + if let error = error as? Mastodon.API.Error { switch error.httpResponseStatus { case .notFound: - let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext - try await backgroundManagedObjectContext.performChanges { - guard let notification = notification.object(in: backgroundManagedObjectContext) else { return } - for feed in notification.feeds { - backgroundManagedObjectContext.delete(feed) - } - backgroundManagedObjectContext.delete(notification) - } + break default: let alertController = await UIAlertController(for: error, title: nil, preferredStyle: .alert) let okAction = await UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) @@ -121,32 +104,14 @@ extension DataSourceFacade { return } - try? await managedObjectContext.performChanges { - guard let notification = notification.object(in: managedObjectContext) else { return } - switch query { - case .accept: - notification.transientFollowRequestState = .init(state: .isAccept) - case .reject: - // do nothing due to will delete notification - break - } + switch query { + case .accept: + notification.transientFollowRequestState = .init(state: .isAccept) + notification.followRequestState = .init(state: .isAccept) + case .reject: + break } - - let backgroundManagedObjectContext = dependency.context.backgroundManagedObjectContext - try? await backgroundManagedObjectContext.performChanges { - guard let notification = notification.object(in: backgroundManagedObjectContext) else { return } - switch query { - case .accept: - notification.followRequestState = .init(state: .isAccept) - case .reject: - // delete notification - for feed in notification.feeds { - backgroundManagedObjectContext.delete(feed) - } - backgroundManagedObjectContext.delete(notification) - } - } - } // end func + } } extension DataSourceFacade { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift index 8379f08e9..45622dba4 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift @@ -9,6 +9,7 @@ import UIKit import CoreDataStack import MastodonUI import MastodonLocalization +import MastodonSDK extension DataSourceFacade { @@ -61,15 +62,12 @@ extension DataSourceFacade { @MainActor static func coordinateToMediaPreviewScene( dependency: NeedsDependency & MediaPreviewableViewController, - status: ManagedObjectRecord, + status: MastodonStatus, previewContext: AttachmentPreviewContext ) async throws { let managedObjectContext = dependency.context.managedObjectContext - let attachments: [MastodonAttachment] = try await managedObjectContext.perform { - guard let _status = status.object(in: managedObjectContext) else { return [] } - let status = _status.reblog ?? _status - return status.attachments - } + let status = status.reblog ?? status + let attachments = status.entity.mastodonAttachments let thumbnails = await previewContext.thumbnails() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift index ca3bbd474..3cb21fb0f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift @@ -9,16 +9,17 @@ import Foundation import CoreDataStack import MetaTextKit import MastodonCore +import MastodonSDK extension DataSourceFacade { static func responseToMetaTextAction( provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, - status: ManagedObjectRecord, + status: MastodonStatus, meta: Meta ) async throws { - let _redirectRecord = await DataSourceFacade.status( + let _redirectRecord = DataSourceFacade.status( managedObjectContext: provider.context.managedObjectContext, status: status, target: target @@ -35,7 +36,7 @@ extension DataSourceFacade { static func responseToMetaTextAction( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, + status: MastodonStatus, meta: Meta ) async { switch meta { @@ -55,7 +56,7 @@ extension DataSourceFacade { url: url ) case .hashtag(_, let hashtag, _): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) + let hashtagTimelineViewModel = await HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) _ = await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: provider, transition: .show) case .mention(_, let mention, let userInfo): await coordinateToProfileScene( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift index efdf41dbd..9364cbc24 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift @@ -9,41 +9,14 @@ import Foundation import CoreData import CoreDataStack import MastodonUI +import MastodonSDK extension DataSourceFacade { static func status( managedObjectContext: NSManagedObjectContext, - status: ManagedObjectRecord, + status: MastodonStatus, target: StatusTarget - ) async -> ManagedObjectRecord? { - return try? await managedObjectContext.perform { - guard let object = status.object(in: managedObjectContext) else { return nil } - return DataSourceFacade.status(status: object, target: target) - .flatMap { ManagedObjectRecord(objectID: $0.objectID) } - } - } -} - -extension DataSourceFacade { - static func author( - managedObjectContext: NSManagedObjectContext, - status: ManagedObjectRecord, - target: StatusTarget - ) async -> ManagedObjectRecord? { - return try? await managedObjectContext.perform { - guard let object = status.object(in: managedObjectContext) else { return nil } - return DataSourceFacade.status(status: object, target: target) - .flatMap { $0.author } - .flatMap { ManagedObjectRecord(objectID: $0.objectID) } - } - } -} - -extension DataSourceFacade { - static func status( - status: Status, - target: StatusTarget - ) -> Status? { + ) -> MastodonStatus? { switch target { case .status: return status.reblog ?? status @@ -52,3 +25,19 @@ extension DataSourceFacade { } } } + +extension DataSourceFacade { + static func author( + managedObjectContext: NSManagedObjectContext, + status: MastodonStatus, + target: StatusTarget + ) async -> ManagedObjectRecord? { + return try? await managedObjectContext.perform { + return DataSourceFacade.status(managedObjectContext: managedObjectContext, status: status, target: target) + .flatMap { $0.entity.account } + .flatMap { + MastodonUser.findOrFetch(in: managedObjectContext, matching: MastodonUser.predicate(domain: $0.domain ?? "", id: $0.id))?.asRecord + } + } + } +} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 30c024f54..3cf14f83b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -15,7 +15,7 @@ extension DataSourceFacade { static func coordinateToProfileScene( provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, - status: ManagedObjectRecord + status: MastodonStatus ) async { let _redirectRecord = await DataSourceFacade.author( managedObjectContext: provider.context.managedObjectContext, @@ -83,9 +83,10 @@ extension DataSourceFacade { extension DataSourceFacade { + @MainActor static func coordinateToProfileScene( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, + status: MastodonStatus, mention: String, // username, userInfo: [AnyHashable: Any]? ) async { @@ -100,11 +101,11 @@ extension DataSourceFacade { let managedObjectContext = provider.context.managedObjectContext let mentions = try? await managedObjectContext.perform { - return status.object(in: managedObjectContext)?.mentions ?? [] + return status.entity.mentions ?? [] } guard let mention = mentions?.first(where: { $0.url == href }) else { - _ = await provider.coordinator.present( + _ = provider.coordinator.present( scene: .safari(url: url), from: provider, transition: .safariPresent(animated: true, completion: nil) @@ -131,7 +132,7 @@ extension DataSourceFacade { } }() - _ = await provider.coordinator.present( + _ = provider.coordinator.present( scene: .profile(viewModel: profileViewModel), from: provider, transition: .show diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index ff3e95820..c16a9e415 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -6,20 +6,20 @@ // import UIKit -import CoreDataStack import MastodonCore import MastodonUI +import MastodonSDK extension DataSourceFacade { static func responseToStatusReblogAction( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord + status: MastodonStatus ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await provider.context.apiService.reblog( - record: status, + status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox ) } // end func diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index d304816d0..fdf39b876 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -14,13 +14,14 @@ import MastodonUI import MastodonLocalization import LinkPresentation import UniformTypeIdentifiers +import MastodonSDK // Delete extension DataSourceFacade { static func responseToDeleteStatus( dependency: NeedsDependency & AuthContextProvider, - status: ManagedObjectRecord + status: MastodonStatus ) async throws { _ = try await dependency.context.apiService.deleteStatus( status: status, @@ -36,7 +37,7 @@ extension DataSourceFacade { @MainActor public static func responseToStatusShareAction( provider: DataSourceProvider, - status: ManagedObjectRecord, + status: MastodonStatus, button: UIButton ) async throws { let activityViewController = try await createActivityViewController( @@ -56,22 +57,22 @@ extension DataSourceFacade { private static func createActivityViewController( dependency: NeedsDependency, - status: ManagedObjectRecord + status: MastodonStatus ) async throws -> UIActivityViewController { - var activityItems: [Any] = try await dependency.context.managedObjectContext.perform { - guard let status = status.object(in: dependency.context.managedObjectContext), - let url = URL(string: status.url ?? status.uri) + var activityItems: [Any] = { + guard let url = URL(string: status.entity.url ?? status.entity.uri) else { return [] } return [ URLActivityItemWithMetadata(url: url) { metadata in - metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))" + metadata.title = "\(status.entity.account.displayName) (@\(status.entity.account.acctWithDomain))" metadata.iconProvider = ImageProvider( - url: status.author.avatarImageURLWithFallback(domain: status.author.domain), + url: status.entity.account.avatarImageURLWithFallback(domain: status.entity.account.domain ?? ""), filter: ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize) ).itemProvider } ] as [Any] - } + }() + var applicationActivities: [UIActivity] = [ SafariActivity(sceneCoordinator: dependency.coordinator), // open URL ] @@ -94,20 +95,12 @@ extension DataSourceFacade { @MainActor static func responseToActionToolbar( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, + status: MastodonStatus, action: ActionToolbarContainer.Action, sender: UIButton ) async throws { let managedObjectContext = provider.context.managedObjectContext - let _status: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let object = status.object(in: managedObjectContext) else { return nil } - let objectID = (object.reblog ?? object).objectID - return .init(objectID: objectID) - } - guard let status = _status else { - assertionFailure() - return - } + let _status = status.reblog ?? status switch action { case .reply: @@ -266,7 +259,7 @@ extension DataSourceFacade { context: dependency.context, authContext: dependency.authContext, user: user, - status: menuContext.statusViewModel?.originalStatus?.asRecord + status: menuContext.statusViewModel?.originalStatus ) _ = dependency.coordinator.present( @@ -297,7 +290,7 @@ extension DataSourceFacade { ) case .bookmarkStatus: Task { - guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { + guard let status = menuContext.statusViewModel?.originalStatus else { assertionFailure() return } @@ -309,11 +302,7 @@ extension DataSourceFacade { case .shareStatus: Task { let managedObjectContext = dependency.context.managedObjectContext - guard let status: ManagedObjectRecord = try? await managedObjectContext.perform(block: { - guard let object = menuContext.statusViewModel?.originalStatus?.asRecord.object(in: managedObjectContext) else { return nil } - let objectID = (object.reblog ?? object).objectID - return .init(objectID: objectID) - }) else { + guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else { assertionFailure() return } @@ -344,7 +333,7 @@ extension DataSourceFacade { style: .destructive ) { [weak dependency] _ in guard let dependency = dependency else { return } - guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { return } + guard let status = menuContext.statusViewModel?.originalStatus else { return } Task { try await DataSourceFacade.responseToDeleteStatus( dependency: dependency, @@ -358,7 +347,7 @@ extension DataSourceFacade { dependency.present(alertController, animated: true) case .translateStatus: - guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { return } + guard let status = menuContext.statusViewModel?.originalStatus else { return } do { let translation = try await DataSourceFacade.translateStatus(provider: dependency,status: status) @@ -371,7 +360,7 @@ extension DataSourceFacade { } case .editStatus: - guard let status = menuContext.statusViewModel?.originalStatus?.asRecord.object(in: dependency.context.managedObjectContext) else { return } + guard let status = menuContext.statusViewModel?.originalStatus else { return } let statusSource = try await dependency.context.apiService.getStatusSource( forStatusID: status.id, @@ -402,12 +391,11 @@ extension DataSourceFacade { static func responseToToggleSensitiveAction( dependency: NeedsDependency, - status: ManagedObjectRecord + status: MastodonStatus ) async throws { try await dependency.context.managedObjectContext.perform { - guard let _status = status.object(in: dependency.context.managedObjectContext) else { return } - let status = _status.reblog ?? _status - status.update(isSensitiveToggled: !status.isSensitiveToggled) + let _status = status.reblog ?? status + _status.isSensitiveToggled = !_status.isSensitiveToggled } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift index ad8d0e671..61075d436 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift @@ -9,15 +9,16 @@ import UIKit import CoreData import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { static func coordinateToStatusThreadScene( provider: ViewControllerWithDependencies & AuthContextProvider, target: StatusTarget, - status: ManagedObjectRecord + status: MastodonStatus ) async { - let _root: StatusItem.Thread? = await { - let _redirectRecord = await DataSourceFacade.status( + let _root: StatusItem.Thread? = { + let _redirectRecord = DataSourceFacade.status( managedObjectContext: provider.context.managedObjectContext, status: status, target: target diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift index 7560d9008..2523be1b4 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift @@ -20,27 +20,11 @@ extension DataSourceFacade { public static func translateStatus( provider: Provider, - status: ManagedObjectRecord + status: MastodonStatus ) async throws -> Mastodon.Entity.Translation? { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - guard - let status = status.object(in: provider.context.managedObjectContext) - else { - return nil - } - - if let reblog = status.reblog { - return try await translateStatus(provider: provider, status: reblog) - } else { - return try await translateStatus(provider: provider, status: status) - } - } -} - -private extension DataSourceFacade { - static func translateStatus(provider: Provider, status: Status) async throws -> Mastodon.Entity.Translation? { do { let value = try await provider.context .apiService diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift b/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift index a65de9537..286618a2c 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift @@ -9,11 +9,12 @@ import Foundation import CoreDataStack import MetaTextKit import MastodonCore +import MastodonSDK extension DataSourceFacade { static func responseToURLAction( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, + status: MastodonStatus, url: URL ) async { let domain = provider.authContext.mastodonAuthenticationBox.domain diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 0974510bf..8d8e62bd9 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -10,6 +10,7 @@ import MetaTextKit import CoreDataStack import MastodonCore import MastodonUI +import MastodonSDK // MARK: - Notification AuthorMenuAction extension NotificationTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { @@ -31,7 +32,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut } let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } return .init(objectID: notification.account.objectID) } guard let author = _author else { @@ -71,7 +71,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } return .init(objectID: notification.account.objectID) } guard let author = _author else { @@ -155,7 +154,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut } private struct NotificationMediaTransitionContext { - let status: ManagedObjectRecord + let status: MastodonStatus let needsToggleMediaSensitive: Bool } @@ -180,16 +179,13 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med return } - let managedObjectContext = self.context.managedObjectContext - let _mediaTransitionContext: NotificationMediaTransitionContext? = try await managedObjectContext.perform { - guard let notification = record.object(in: managedObjectContext) else { return nil } - guard let _status = notification.status else { return nil } - let status = _status.reblog ?? _status + let _mediaTransitionContext: NotificationMediaTransitionContext? = { + guard let status = record.status?.reblog ?? record.status else { return nil } return NotificationMediaTransitionContext( - status: .init(objectID: status.objectID), - needsToggleMediaSensitive: status.isSensitiveToggled ? !status.sensitive : status.sensitive + status: status, + needsToggleMediaSensitive: status.isSensitiveToggled ? !(status.entity.sensitive == true) : (status.entity.sensitive == true) ) - } + }() guard let mediaTransitionContext = _mediaTransitionContext else { return } @@ -233,15 +229,13 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med } let managedObjectContext = self.context.managedObjectContext - let _mediaTransitionContext: NotificationMediaTransitionContext? = try await managedObjectContext.perform { - guard let notification = record.object(in: managedObjectContext) else { return nil } - guard let _status = notification.status else { return nil } - let status = _status.reblog ?? _status + let _mediaTransitionContext: NotificationMediaTransitionContext? = { + guard let status = record.status?.reblog ?? record.status else { return nil } return NotificationMediaTransitionContext( - status: .init(objectID: status.objectID), - needsToggleMediaSensitive: status.isMediaSensitive ? !status.isSensitiveToggled : false + status: status, + needsToggleMediaSensitive: status.entity.sensitive == true ? !status.isSensitiveToggled : false ) - } + }() guard let mediaTransitionContext = _mediaTransitionContext else { return } @@ -286,12 +280,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for status data provider") return } - let _status: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.objectID) - } - guard let status = _status else { + guard let status = notification.status?.reblog ?? notification.status else { assertionFailure() return } @@ -323,18 +312,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for status data provider") return } - let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.author.objectID) - } - guard let author = _author else { - assertionFailure() - return - } + await DataSourceFacade.coordinateToProfileScene( provider: self, - user: author + user: notification.account.asRecord ) } // end Task } @@ -367,12 +348,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for notification item") return } - let _status: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.objectID) - } - guard let status = _status else { + guard let status = notification.status?.reblog ?? notification.status else { assertionFailure() return } @@ -400,12 +376,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for notification item") return } - let _status: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.objectID) - } - guard let status = _status else { + guard let status = notification.status?.reblog ?? notification.status else { assertionFailure() return } @@ -465,12 +436,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for notification item") return } - let _status: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.objectID) - } - guard let status = _status else { + guard let status = notification.status?.reblog ?? notification.status else { assertionFailure() return } @@ -497,12 +463,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for notification item") return } - let _status: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let notification = notification.object(in: self.context.managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.objectID) - } - guard let status = _status else { + guard let status = notification.status?.reblog ?? notification.status else { assertionFailure() return } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 68be145e2..554bb92f1 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -38,10 +38,9 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte break case .reply: let _replyToAuthor: ManagedObjectRecord? = try? await context.managedObjectContext.perform { - guard let status = status.object(in: self.context.managedObjectContext) else { return nil } - guard let inReplyToAccountID = status.inReplyToAccountID else { return nil } + guard let inReplyToAccountID = status.entity.inReplyToAccountID else { return nil } let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: status.author.domain, id: inReplyToAccountID) + request.predicate = MastodonUser.predicate(domain: status.entity.account.domain ?? "", id: inReplyToAccountID) request.fetchLimit = 1 guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } return .init(objectID: author.objectID) @@ -184,7 +183,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte cardControlMenu statusCardControl: StatusCardControl ) -> [LabeledAction]? { guard let card = statusView.viewModel.card, - let url = card.url else { + let url = URL(string: card.url) else { return nil } @@ -206,8 +205,8 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte URLActivityItemWithMetadata(url: url) { metadata in metadata.title = card.title - if let image = card.imageURL { - metadata.iconProvider = ImageProvider(url: image, filter: nil).itemProvider + if let image = card.image, let url = URL(string: image) { + metadata.iconProvider = ImageProvider(url: url, filter: nil).itemProvider } } ], @@ -471,8 +470,10 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte return } let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - guard let _status = status.object(in: self.context.managedObjectContext) else { return nil } - let author = (_status.reblog ?? _status).author + let request = MastodonUser.sortedFetchRequest + request.predicate = MastodonUser.predicate(domain: status.entity.account.domain ?? "", id: status.entity.account.id) + request.fetchLimit = 1 + guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } return .init(objectID: author.objectID) } guard let author = _author else { @@ -679,11 +680,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte assertionFailure("only works for status data provider") return } - - guard let status = status.object(in: context.managedObjectContext) else { - return await coordinator.hideLoading() - } - + do { let edits = try await context.apiService.getHistory(forStatusID: status.id, authenticationBox: authContext.mastodonAuthenticationBox).value diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift index f90827863..97081cc6f 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -8,6 +8,7 @@ import UIKit import CoreDataStack import MastodonCore +import MastodonSDK extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay { @@ -55,7 +56,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider { @MainActor - private func statusRecord() async -> ManagedObjectRecord? { + private func statusRecord() async -> MastodonStatus? { guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return nil } let source = DataSourceItem.Source(indexPath: indexPathForSelectedRow) guard let item = await item(from: source) else { return nil } @@ -64,12 +65,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid case .status(let record): return record case .notification(let record): - let _statusRecord: ManagedObjectRecord? = try? await context.managedObjectContext.perform { - guard let notification = record.object(in: self.context.managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.objectID) - } - guard let statusRecord = _statusRecord else { + guard let statusRecord = record.status else { return nil } return statusRecord diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 0944cee6c..41838b0e6 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -10,6 +10,7 @@ import CoreDataStack import MastodonCore import MastodonUI import MastodonLocalization +import MastodonSDK extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { @@ -42,11 +43,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid case .notification(let notification): let managedObjectContext = context.managedObjectContext - let _status: ManagedObjectRecord? = try await managedObjectContext.perform { - guard let notification = notification.object(in: managedObjectContext) else { return nil } - guard let status = notification.status else { return nil } - return .init(objectID: status.objectID) - } + let _status: MastodonStatus? = notification.status if let status = _status { await DataSourceFacade.coordinateToStatusThreadScene( provider: self, @@ -54,10 +51,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid status: status ) } else { - let _author: ManagedObjectRecord? = try await managedObjectContext.perform { - guard let notification = notification.object(in: managedObjectContext) else { return nil } - return .init(objectID: notification.account.objectID) - } + let _author: ManagedObjectRecord? = notification.account.asRecord if let author = _author { await DataSourceFacade.coordinateToProfileScene( provider: self, diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index b92aadcef..ab6df8c2c 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -12,10 +12,10 @@ import MastodonSDK import class CoreDataStack.Notification enum DataSourceItem: Hashable { - case status(record: ManagedObjectRecord) + case status(record: MastodonStatus) case user(record: ManagedObjectRecord) case hashtag(tag: TagKind) - case notification(record: ManagedObjectRecord) + case notification(record: MastodonNotification) case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) } diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 442a47de4..798e198f1 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -21,7 +21,7 @@ final class ComposeViewModel { enum Context { case composeStatus - case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource) + case editStatus(status: MastodonStatus, statusSource: Mastodon.Entity.StatusSource) } var disposeBag = Set() diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift index f61df078d..6dccb355d 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift @@ -145,10 +145,10 @@ extension DiscoveryCommunityViewModel.State { self.maxID = newMaxID var hasNewStatusesAppend = false - var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs + var statusIDs = isReloading ? [] : await viewModel.statusFetchedResultsController.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !statusIDs.contains(where: { $0.id == status.id }) else { continue } + statusIDs.append(.fromEntity(status)) hasNewStatusesAppend = true } @@ -158,7 +158,7 @@ extension DiscoveryCommunityViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + await viewModel.statusFetchedResultsController.setRecords(statusIDs) viewModel.didLoadLatest.send() } catch { diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift index 6169e0830..34b9895b2 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift @@ -41,14 +41,11 @@ final class DiscoveryCommunityViewModel { let didLoadLatest = PassthroughSubject() + @MainActor init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.statusFetchedResultsController = StatusFetchedResultsController() // end init } } diff --git a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift index 244a2e8d4..b1403f693 100644 --- a/Mastodon/Scene/Discovery/DiscoveryViewModel.swift +++ b/Mastodon/Scene/Discovery/DiscoveryViewModel.swift @@ -27,6 +27,7 @@ final class DiscoveryViewModel { @Published var viewControllers: [ScrollViewContainer & PageViewController] + @MainActor init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) { self.context = context self.authContext = authContext diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift index 75794258d..47dabe5a1 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift @@ -143,10 +143,10 @@ extension DiscoveryPostsViewModel.State { self.offset = newOffset var hasNewStatusesAppend = false - var statusIDs = isReloading ? [] : viewModel.statusFetchedResultsController.statusIDs + var statusIDs = isReloading ? [] : await viewModel.statusFetchedResultsController.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !statusIDs.contains(where: { $0.id == status.id }) else { continue } + statusIDs.append(.fromEntity(status)) hasNewStatusesAppend = true } @@ -155,7 +155,7 @@ extension DiscoveryPostsViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + await viewModel.statusFetchedResultsController.setRecords(statusIDs) viewModel.didLoadLatest.send() } catch { diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift index 3024f03be..d7ea73e9b 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift @@ -41,14 +41,11 @@ final class DiscoveryPostsViewModel { let didLoadLatest = PassthroughSubject() @Published var isServerSupportEndpoint = true + @MainActor init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.statusFetchedResultsController = StatusFetchedResultsController() // end init Task { diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index 579060bda..aa4d89d77 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -8,6 +8,7 @@ import Foundation import GameplayKit import CoreDataStack +import MastodonSDK extension HashtagTimelineViewModel { class State: GKState { @@ -145,10 +146,10 @@ extension HashtagTimelineViewModel.State { self.maxID = newMaxID var hasNewStatusesAppend = false - var statusIDs = isReloading ? [] : viewModel.fetchedResultsController.statusIDs + var statusIDs = isReloading ? [] : await viewModel.fetchedResultsController.records.map { $0.entity } for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !statusIDs.contains(status) else { continue } + statusIDs.append(status) hasNewStatusesAppend = true } @@ -158,7 +159,7 @@ extension HashtagTimelineViewModel.State { await enter(state: NoMore.self) } - viewModel.fetchedResultsController.append(statusIDs: statusIDs) + await viewModel.fetchedResultsController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) }) viewModel.didLoadLatest.send() } catch { await enter(state: Fail.self) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index fbdc42a1c..84f8a62aa 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -50,15 +50,12 @@ final class HashtagTimelineViewModel { return stateMachine }() + @MainActor init(context: AppContext, authContext: AuthContext, hashtag: String) { self.context = context self.authContext = authContext self.hashtag = hashtag - self.fetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.fetchedResultsController = StatusFetchedResultsController() updateTagInformation() // end init } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift index b141d386a..73e72170c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift @@ -20,17 +20,16 @@ extension HomeTimelineViewController: DataSourceProvider { } switch item { - case .feed(let record): + case .feed(let feed): let managedObjectContext = context.managedObjectContext - let item: DataSourceItem? = try? await managedObjectContext.perform { - guard let feed = record.object(in: managedObjectContext) else { return nil } + let item: DataSourceItem? = { guard feed.kind == .home else { return nil } if let status = feed.status { - return .status(record: .init(objectID: status.objectID)) + return .status(record: status) } else { return nil } - } + }() return item default: return nil diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 18cbf18d2..769ea44cc 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -52,37 +52,37 @@ extension HomeTimelineViewModel { snapshot.appendItems(newItems, toSection: .main) return snapshot }() - - let parentManagedObjectContext = self.context.managedObjectContext - let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - managedObjectContext.parent = parentManagedObjectContext - try? await managedObjectContext.perform { - let anchors: [Feed] = { - let request = Feed.sortedFetchRequest - request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - Feed.hasMorePredicate(), - self.fetchedResultsController.predicate, - ]) - do { - return try managedObjectContext.fetch(request) - } catch { - assertionFailure(error.localizedDescription) - return [] - } - }() - - let itemIdentifiers = newSnapshot.itemIdentifiers - for (index, item) in itemIdentifiers.enumerated() { - guard case let .feed(record) = item else { continue } - guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue } - let isLast = index + 1 == itemIdentifiers.count - if isLast { - newSnapshot.insertItems([.bottomLoader], afterItem: item) - } else { - newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) - } - } - } + #warning("We probably need to replace the code below") +// let parentManagedObjectContext = self.context.managedObjectContext +// let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) +// managedObjectContext.parent = parentManagedObjectContext +// try? await managedObjectContext.perform { +// let anchors: [Feed] = { +// let request = Feed.sortedFetchRequest +// request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ +// Feed.hasMorePredicate(), +// self.fetchedResultsController.predicate, +// ]) +// do { +// return try managedObjectContext.fetch(request) +// } catch { +// assertionFailure(error.localizedDescription) +// return [] +// } +// }() +// +// let itemIdentifiers = newSnapshot.itemIdentifiers +// for (index, item) in itemIdentifiers.enumerated() { +// guard case let .feed(record) = item else { continue } +// guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue } +// let isLast = index + 1 == itemIdentifiers.count +// if isLast { +// newSnapshot.insertItems([.bottomLoader], afterItem: item) +// } else { +// newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) +// } +// } +// } let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers if !hasChanges && !self.hasPendingStatusEditReload { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 7f056928c..cfad3f24c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -84,14 +84,10 @@ extension HomeTimelineViewModel.LoadLatestState { guard let viewModel else { return } let latestFeedRecords = viewModel.fetchedResultsController.records.prefix(APIService.onceRequestStatusMaxCount) - let parentManagedObjectContext = viewModel.fetchedResultsController.managedObjectContext - let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - managedObjectContext.parent = parentManagedObjectContext Task { let latestStatusIDs: [Status.ID] = latestFeedRecords.compactMap { record in - guard let feed = record.object(in: managedObjectContext) else { return nil } - return feed.status?.id + return record.status?.id } do { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 1b6e4499d..a2bf3e224 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -52,13 +52,10 @@ extension HomeTimelineViewModel.LoadOldestState { } Task { - let managedObjectContext = viewModel.fetchedResultsController.managedObjectContext - let _maxID: Mastodon.Entity.Status.ID? = try await managedObjectContext.perform { - guard let feed = lastFeedRecord.object(in: managedObjectContext), - let status = feed.status - else { return nil } + let _maxID: Mastodon.Entity.Status.ID? = { + guard let status = lastFeedRecord.status else { return nil } return status.id - } + }() guard let maxID = _maxID else { await self.enter(state: Fail.self) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 040fe45b5..196b72152 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -80,15 +80,9 @@ final class HomeTimelineViewModel: NSObject { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) + self.fetchedResultsController = FeedFetchedResultsController() self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) super.init() - - fetchedResultsController.predicate = Feed.predicate( - kind: .home, - acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID) - ) - homeTimelineNeedRefresh .sink { [weak self] _ in self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self) @@ -116,7 +110,7 @@ extension HomeTimelineViewModel { extension HomeTimelineViewModel { func timelineDidReachEnd() { - fetchedResultsController.fetchNextBatch() + #warning("Check if required, e.g. when locally caching MastodonStatus") } } @@ -128,47 +122,41 @@ extension HomeTimelineViewModel { guard let diffableDataSource = diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() - let managedObjectContext = context.managedObjectContext - let key = "LoadMore@\(record.objectID)" - guard let feed = record.object(in: managedObjectContext) else { return } - guard let status = feed.status else { return } +// let managedObjectContext = context.managedObjectContext +// let key = "LoadMore@\(record.objectID)" +// +// guard let feed = record.object(in: managedObjectContext) else { return } - // keep transient property live - managedObjectContext.cache(feed, key: key) - defer { - managedObjectContext.cache(nil, key: key) - } - do { - // update state - try await managedObjectContext.performChanges { - feed.update(isLoadingMore: true) - } - } catch { - assertionFailure(error.localizedDescription) - } + guard let status = record.status else { return } + record.isLoadingMore = true + +// // keep transient property live +// managedObjectContext.cache(feed, key: key) +// defer { +// managedObjectContext.cache(nil, key: key) +// } +// do { +// // update state +// try await managedObjectContext.performChanges { +// feed.update(isLoadingMore: true) +// } +// } catch { +// assertionFailure(error.localizedDescription) +// } // reconfigure item snapshot.reconfigureItems([item]) await updateSnapshotUsingReloadData(snapshot: snapshot) // fetch data - do { - let maxID = status.id - _ = try await context.apiService.homeTimeline( - maxID: maxID, - authenticationBox: authContext.mastodonAuthenticationBox - ) - } catch { - do { - // restore state - try await managedObjectContext.performChanges { - feed.update(isLoadingMore: false) - } - } catch { - assertionFailure(error.localizedDescription) - } - } + let maxID = status.id + _ = try? await context.apiService.homeTimeline( + maxID: maxID, + authenticationBox: authContext.mastodonAuthenticationBox + ) + + record.isLoadingMore = false // reconfigure item again snapshot.reconfigureItems([item]) diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift index 1d2b40ebc..135a7adc2 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift @@ -8,6 +8,7 @@ import UIKit import Combine import CoreDataStack +import MastodonSDK extension NotificationTableViewCell { final class ViewModel { @@ -18,7 +19,7 @@ extension NotificationTableViewCell { } enum Value { - case feed(Feed) + case feed(MastodonFeed) } } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index c058ee921..5ccc2a2ff 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension NotificationTimelineViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -20,17 +21,16 @@ extension NotificationTimelineViewController: DataSourceProvider { } switch item { - case .feed(let record): + case .feed(let feed): let managedObjectContext = context.managedObjectContext - let item: DataSourceItem? = try? await managedObjectContext.perform { - guard let feed = record.object(in: managedObjectContext) else { return nil } + let item: DataSourceItem? = { guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil } - if let notification = feed.notification { - return .notification(record: .init(objectID: notification.objectID)) + if let notification = feed.notification, let mastodonNotification = MastodonNotification.fromEntity(notification, using: managedObjectContext) { + return .notification(record: mastodonNotification) } else { return nil } - } + }() return item default: return nil diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index d081327d3..d4c68c09f 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -280,14 +280,13 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { Task { @MainActor in switch item { case .feed(let record): - guard let feed = record.object(in: self.context.managedObjectContext) else { return } - guard let notification = feed.notification else { return } + guard let notification = record.notification else { return } - if let stauts = notification.status { + if let status = notification.status { let threadViewModel = ThreadViewModel( context: self.context, authContext: self.viewModel.authContext, - optionalRoot: .root(context: .init(status: .init(objectID: stauts.objectID))) + optionalRoot: .root(context: .init(status: .fromEntity(status))) ) _ = self.coordinator.present( scene: .thread(viewModel: threadViewModel), @@ -295,16 +294,25 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { transition: .show ) } else { - let profileViewModel = ProfileViewModel( - context: self.context, - authContext: self.viewModel.authContext, - optionalMastodonUser: notification.account - ) - _ = self.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: self, - transition: .show - ) + context.managedObjectContext.perform { + let mastodonUserRequest = MastodonUser.sortedFetchRequest + mastodonUserRequest.predicate = MastodonUser.predicate(domain: notification.account.domain ?? "", id: notification.account.id) + mastodonUserRequest.fetchLimit = 1 + guard let mastodonUser = try? self.context.managedObjectContext.fetch(mastodonUserRequest).first else { + return + } + + let profileViewModel = ProfileViewModel( + context: self.context, + authContext: self.viewModel.authContext, + optionalMastodonUser: mastodonUser + ) + _ = self.coordinator.present( + scene: .profile(viewModel: profileViewModel), + from: self, + transition: .show + ) + } } default: break diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index c412c39a4..e3460a72b 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -48,36 +48,37 @@ extension NotificationTimelineViewModel { return snapshot }() - let parentManagedObjectContext = self.context.managedObjectContext - let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - managedObjectContext.parent = parentManagedObjectContext - try? await managedObjectContext.perform { - let anchors: [Feed] = { - let request = Feed.sortedFetchRequest - request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ - Feed.hasMorePredicate(), - self.feedFetchedResultsController.predicate, - ]) - do { - return try managedObjectContext.fetch(request) - } catch { - assertionFailure(error.localizedDescription) - return [] - } - }() - - let itemIdentifiers = newSnapshot.itemIdentifiers - for (index, item) in itemIdentifiers.enumerated() { - guard case let .feed(record) = item else { continue } - guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue } - let isLast = index + 1 == itemIdentifiers.count - if isLast { - newSnapshot.insertItems([.bottomLoader], afterItem: item) - } else { - newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) - } - } - } + #warning("Do we still need the code below?") +// let parentManagedObjectContext = self.context.managedObjectContext +// let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) +// managedObjectContext.parent = parentManagedObjectContext +// try? await managedObjectContext.perform { +// let anchors: [Feed] = { +// let request = Feed.sortedFetchRequest +// request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ +// Feed.hasMorePredicate(), +// self.feedFetchedResultsController.predicate, +// ]) +// do { +// return try managedObjectContext.fetch(request) +// } catch { +// assertionFailure(error.localizedDescription) +// return [] +// } +// }() +// +// let itemIdentifiers = newSnapshot.itemIdentifiers +// for (index, item) in itemIdentifiers.enumerated() { +// guard case let .feed(record) = item else { continue } +// guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue } +// let isLast = index + 1 == itemIdentifiers.count +// if isLast { +// newSnapshot.insertItems([.bottomLoader], afterItem: item) +// } else { +// newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) +// } +// } +// } let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers if !hasChanges { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index 3be724701..39d97ba6e 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -55,12 +55,7 @@ extension NotificationTimelineViewModel.LoadOldestState { Task { let managedObjectContext = viewModel.context.managedObjectContext - let _maxID: Mastodon.Entity.Notification.ID? = try await managedObjectContext.perform { - guard let feed = lastFeedRecord.object(in: managedObjectContext), - let notification = feed.notification - else { return nil } - return notification.id - } + let _maxID: Mastodon.Entity.Notification.ID? = lastFeedRecord.notification?.id guard let maxID = _maxID else { await self.enter(state: Fail.self) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index a6412d365..f4c082909 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -43,6 +43,7 @@ final class NotificationTimelineViewModel { return stateMachine }() + @MainActor init( context: AppContext, authContext: AuthContext, @@ -51,13 +52,7 @@ final class NotificationTimelineViewModel { self.context = context self.authContext = authContext self.scope = scope - self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) - // end init - - feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate( - authenticationBox: authContext.mastodonAuthenticationBox, - scope: scope - ) + self.feedFetchedResultsController = FeedFetchedResultsController() } @@ -125,29 +120,16 @@ extension NotificationTimelineViewModel { // load timeline gap func loadMore(item: NotificationItem) async { guard case let .feedLoader(record) = item else { return } - - let managedObjectContext = context.managedObjectContext - let key = "LoadMore@\(record.objectID)" - - // return when already loading state - guard managedObjectContext.cache(froKey: key) == nil else { return } - guard let feed = record.object(in: managedObjectContext) else { return } - guard let maxID = feed.notification?.id else { return } - // keep transient property live - managedObjectContext.cache(feed, key: key) - defer { - managedObjectContext.cache(nil, key: key) - } - + guard let maxID = record.notification?.id else { return } + // fetch data - do { - _ = try await context.apiService.notifications( - maxID: maxID, - scope: scope, - authenticationBox: authContext.mastodonAuthenticationBox - ) - } catch { + if let notifications = try? await context.apiService.notifications( + maxID: maxID, + scope: scope, + authenticationBox: authContext.mastodonAuthenticationBox + ) { + self.feedFetchedResultsController.records += notifications.value.map { MastodonFeed.fromNotification($0, kind: record.kind) } } } diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift index f746a5acb..66ffe866f 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -57,7 +57,9 @@ extension BookmarkViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.statusFetchedResultsController.statusIDs = [] + DispatchQueue.main.async { + viewModel.statusFetchedResultsController.reset() + } stateMachine.enter(Loading.self) } @@ -128,10 +130,10 @@ extension BookmarkViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var statusIDs = await viewModel.statusFetchedResultsController.records.map { $0.entity } for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !statusIDs.contains(status) else { continue } + statusIDs.append(status) hasNewStatusesAppend = true } @@ -147,7 +149,9 @@ extension BookmarkViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + + await viewModel.statusFetchedResultsController.setRecords(statusIDs.map { MastodonStatus.fromEntity($0) }) + } catch { await enter(state: Fail.self) } diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift index f56e65526..260e65e9b 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift @@ -38,14 +38,11 @@ final class BookmarkViewModel { return stateMachine }() + @MainActor init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.statusFetchedResultsController = StatusFetchedResultsController() } } diff --git a/Mastodon/Scene/Profile/CachedProfileViewModel.swift b/Mastodon/Scene/Profile/CachedProfileViewModel.swift index a769f2a9f..afefd4cdf 100644 --- a/Mastodon/Scene/Profile/CachedProfileViewModel.swift +++ b/Mastodon/Scene/Profile/CachedProfileViewModel.swift @@ -11,6 +11,7 @@ import MastodonCore final class CachedProfileViewModel: ProfileViewModel { + @MainActor init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) { super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser) } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift index a1a8d0f99..10f6f269a 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift @@ -55,10 +55,12 @@ extension FavoriteViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - // reset - viewModel.statusFetchedResultsController.statusIDs = [] - - stateMachine.enter(Loading.self) + Task { + // reset + await viewModel.statusFetchedResultsController.reset() + + stateMachine.enter(Loading.self) + } } } @@ -127,10 +129,10 @@ extension FavoriteViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var statusIDs = await viewModel.statusFetchedResultsController.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !statusIDs.contains(where: { $0.id == status.id }) else { continue } + statusIDs.append(.fromEntity(status)) hasNewStatusesAppend = true } @@ -146,7 +148,7 @@ extension FavoriteViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + await viewModel.statusFetchedResultsController.setRecords(statusIDs) } catch { await enter(state: Fail.self) } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift index 0dd3c7203..b79bbbbfc 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift @@ -37,14 +37,11 @@ final class FavoriteViewModel { return stateMachine }() + @MainActor init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.statusFetchedResultsController = StatusFetchedResultsController() } } diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index 7f88d2ffe..ecbaef01e 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -14,6 +14,7 @@ import MastodonSDK final class MeProfileViewModel: ProfileViewModel { + @MainActor init(context: AppContext, authContext: AuthContext) { let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) super.init( diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 630205371..8ed3cf03d 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -56,6 +56,7 @@ class ProfileViewModel: NSObject { // @Published var protected: Bool? = nil // let needsPagePinToTop = CurrentValueSubject(false) + @MainActor init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) { self.context = context self.authContext = authContext diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 832c25858..b0a2f9f48 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -13,6 +13,7 @@ import MastodonCore final class RemoteProfileViewModel: ProfileViewModel { + @MainActor init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) { super.init(context: context, authContext: authContext, optionalMastodonUser: nil) @@ -51,6 +52,7 @@ final class RemoteProfileViewModel: ProfileViewModel { .store(in: &disposeBag) } + @MainActor init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) { super.init(context: context, authContext: authContext, optionalMastodonUser: nil) @@ -89,6 +91,7 @@ final class RemoteProfileViewModel: ProfileViewModel { } // end Task } + @MainActor init(context: AppContext, authContext: AuthContext, acct: String){ super.init(context: context, authContext: authContext, optionalMastodonUser: nil) diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift index cd0110a87..5469cc4bb 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -54,11 +54,13 @@ extension UserTimelineViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - - // reset - viewModel.statusFetchedResultsController.statusIDs = [] - stateMachine.enter(Loading.self) + Task { + // reset + await viewModel.statusFetchedResultsController.reset() + + stateMachine.enter(Loading.self) + } } } @@ -112,17 +114,17 @@ extension UserTimelineViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - let maxID = viewModel.statusFetchedResultsController.statusIDs.last - - guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else { - stateMachine.enter(Fail.self) - return - } - - let queryFilter = viewModel.queryFilter Task { - + let maxID = await viewModel.statusFetchedResultsController.records.last?.id + + guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else { + stateMachine.enter(Fail.self) + return + } + + let queryFilter = viewModel.queryFilter + do { let response = try await viewModel.context.apiService.userTimeline( accountID: userID, @@ -135,10 +137,10 @@ extension UserTimelineViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var statusIDs = await viewModel.statusFetchedResultsController.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !statusIDs.contains(where: { $0.id == status.id }) else { continue } + statusIDs.append(.fromEntity(status)) hasNewStatusesAppend = true } @@ -147,7 +149,7 @@ extension UserTimelineViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + await viewModel.statusFetchedResultsController.setRecords(statusIDs) } catch { await enter(state: Fail.self) diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index 0c8d634e5..f2b67c3ee 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -48,6 +48,7 @@ final class UserTimelineViewModel { return stateMachine }() + @MainActor init( context: AppContext, authContext: AuthContext, @@ -57,11 +58,7 @@ final class UserTimelineViewModel { self.context = context self.authContext = authContext self.title = title - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.statusFetchedResultsController = StatusFetchedResultsController() self.queryFilter = queryFilter } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift index d27562b94..9e26b81e6 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift @@ -10,6 +10,7 @@ import Combine import CoreDataStack import GameplayKit import MastodonCore +import MastodonSDK final class UserListViewModel { var disposeBag = Set() @@ -55,7 +56,7 @@ final class UserListViewModel { extension UserListViewModel { // TODO: refactor follower and following into user list enum Kind { - case rebloggedBy(status: ManagedObjectRecord) - case favoritedBy(status: ManagedObjectRecord) + case rebloggedBy(status: MastodonStatus) + case favoritedBy(status: MastodonStatus) } } diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index cb840d213..ba71da66f 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -29,17 +29,18 @@ class ReportViewModel { let context: AppContext let authContext: AuthContext let user: ManagedObjectRecord - let status: ManagedObjectRecord? + let status: MastodonStatus? // output @Published var isReporting = false @Published var isReportSuccess = false + @MainActor init( context: AppContext, authContext: AuthContext, user: ManagedObjectRecord, - status: ManagedObjectRecord? + status: MastodonStatus? ) { self.context = context self.authContext = authContext @@ -101,17 +102,15 @@ extension ReportViewModel { // the status picker is essential step in report flow // only check isSkip or not - let statusIDs: [Status.ID]? = { + let statusIDs: [MastodonStatus.ID]? = { if self.reportStatusViewModel.isSkip { - let _id: Status.ID? = self.reportStatusViewModel.status.flatMap { record -> Status.ID? in - guard let status = record.object(in: managedObjectContext) else { return nil } - return status.id + let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in + return record.id } - return _id.flatMap { [$0] } + return _id.flatMap { [$0] } ?? [] } else { - return self.reportStatusViewModel.selectStatuses.compactMap { record -> Status.ID? in - guard let status = record.object(in: managedObjectContext) else { return nil } - return status.id + return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in + return record.id } } }() diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift index d4fb507b2..0bb2e0cef 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -64,9 +64,10 @@ extension ReportStatusViewModel.State { super.didEnter(from: previousState) guard let viewModel else { return } - let maxID = viewModel.statusFetchedResultsController.statusIDs.last Task { + let maxID = await viewModel.statusFetchedResultsController.records.last?.id + let managedObjectContext = viewModel.context.managedObjectContext let _userID: MastodonUser.ID? = try await managedObjectContext.perform { guard let user = viewModel.user.object(in: managedObjectContext) else { return nil } @@ -89,10 +90,10 @@ extension ReportStatusViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var statusIDs = await viewModel.statusFetchedResultsController.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !statusIDs.contains(where: { $0.id == status.id }) else { continue } + statusIDs.append(.fromEntity(status)) hasNewStatusesAppend = true } @@ -101,7 +102,7 @@ extension ReportStatusViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + await viewModel.statusFetchedResultsController.setRecords(statusIDs) } catch { await enter(state: Fail.self) diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index 8c41e1ce0..186a2806c 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -25,12 +25,12 @@ class ReportStatusViewModel { let context: AppContext let authContext: AuthContext let user: ManagedObjectRecord - let status: ManagedObjectRecord? + let status: MastodonStatus? let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @Published var isSkip = false - @Published var selectStatuses = OrderedSet>() + @Published var selectStatuses = OrderedSet() // output var diffableDataSource: UITableViewDiffableDataSource? @@ -48,21 +48,18 @@ class ReportStatusViewModel { @Published var isNextButtonEnabled = false + @MainActor init( context: AppContext, authContext: AuthContext, user: ManagedObjectRecord, - status: ManagedObjectRecord? + status: MastodonStatus? ) { self.context = context self.authContext = authContext self.user = user self.status = status - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.statusFetchedResultsController = StatusFetchedResultsController() // end init if let status = status { diff --git a/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift index 00d079cfa..a5ad90bc4 100644 --- a/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift @@ -6,13 +6,13 @@ // import UIKit -import CoreDataStack +import MastodonSDK extension ReportStatusTableViewCell { final class ViewModel { - let value: Status + let value: MastodonStatus - init(value: Status) { + init(value: MastodonStatus) { self.value = value } } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index cd0804b24..036349690 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -87,7 +87,8 @@ class MainTabBarController: UITabBarController { return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80)) } - func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController { + @MainActor + func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) async -> UIViewController { guard let authContext = authContext else { return UITableViewController() } @@ -170,21 +171,26 @@ extension MainTabBarController { view.backgroundColor = .systemBackground // seealso: `ThemeService.apply(theme:)` - let tabs = Tab.allCases - let viewControllers: [UIViewController] = tabs.map { tab in - 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) - return viewController + 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) + } + + _viewControllers = viewControllers + setViewControllers(viewControllers, animated: false) + selectedIndex = 0 } - _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)) { diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift index 66ec904f7..3c82636f3 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift @@ -75,22 +75,10 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl showProfile(viewController, for: account) } else if let status = searchResult.statuses.first { - let status = try await managedObjectContext.perform { - return Persistence.Status.fetch(in: managedObjectContext, context: Persistence.Status.PersistContext( - domain: authContext.mastodonAuthenticationBox.domain, - entity: status, - me: authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext), - statusCache: nil, - userCache: nil, - networkDate: Date())) - } - - guard let status else { return } - await DataSourceFacade.coordinateToStatusThreadScene( provider: viewController, target: .status, // remove reblog wrapper - status: status.asRecord + status: MastodonStatus.fromEntity(status) ) } else if let url = URL(string: urlString) { let prefixedURL: URL? diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift index 813836925..0ee40d384 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift @@ -12,7 +12,7 @@ import MastodonSDK enum SearchResultItem: Hashable { case user(ManagedObjectRecord) - case status(ManagedObjectRecord) + case status(MastodonStatus) case hashtag(tag: Mastodon.Entity.Tag) case bottomLoader(attribute: BottomLoaderAttribute) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index 32a913587..e1d7b294e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -58,18 +58,15 @@ extension SearchResultSection { ) } return cell - case .status(let record): + case .status(let status): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { return } - configure( - context: context, - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .status(status)), - configuration: configuration - ) - } + configure( + context: context, + tableView: tableView, + cell: cell, + viewModel: StatusTableViewCell.ViewModel(value: .status(status)), + configuration: configuration + ) return cell case .hashtag(let tag): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: HashtagTableViewCell.self)) as! HashtagTableViewCell diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index e332b13d9..cc8c46c64 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -124,7 +124,7 @@ extension SearchResultViewModel.State { guard stateMachine.currentState is Loading else { return } let userIDs = response.value.accounts.map { $0.id } - let statusIDs = response.value.statuses.map { $0.id } + let statusIDs = response.value.statuses.map { MastodonStatus.fromEntity($0) } let isNoMore = userIDs.isEmpty && statusIDs.isEmpty @@ -137,12 +137,12 @@ extension SearchResultViewModel.State { // reset data source when the search is refresh if offset == nil { viewModel.userFetchedResultsController.userIDs = [] - viewModel.statusFetchedResultsController.statusIDs = [] + await viewModel.statusFetchedResultsController.reset() viewModel.hashtags = [] } viewModel.userFetchedResultsController.append(userIDs: userIDs) - viewModel.statusFetchedResultsController.append(statusIDs: statusIDs) + await viewModel.statusFetchedResultsController.appendRecords(statusIDs) var hashtags = viewModel.hashtags for hashtag in response.value.hashtags where !hashtags.contains(hashtag) { diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index c3ddc2f0a..43e678a83 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -45,6 +45,7 @@ final class SearchResultViewModel { }() let didDataSourceUpdate = PassthroughSubject() + @MainActor init(context: AppContext, authContext: AuthContext, searchScope: SearchScope = .all, searchText: String) { self.context = context self.authContext = authContext @@ -56,10 +57,6 @@ final class SearchResultViewModel { domain: authContext.mastodonAuthenticationBox.domain, additionalPredicate: nil ) - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) + self.statusFetchedResultsController = StatusFetchedResultsController() } } diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index 576d2eadf..9600461df 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -16,30 +16,29 @@ import MastodonAsset import MastodonCore import MastodonLocalization import class CoreDataStack.Notification +import MastodonSDK extension NotificationView { - public func configure(feed: Feed) { - guard let notification = feed.notification else { + public func configure(feed: MastodonFeed) { + guard + let notification = feed.notification, + let managedObjectContext = viewModel.context?.managedObjectContext + else { assertionFailure() return } - - configure(notification: notification) + + MastodonNotification.fromEntity(notification, using: managedObjectContext).map(configure(notification:)) } } extension NotificationView { - public func configure(notification: Notification) { + public func configure(notification: MastodonNotification) { viewModel.objects.insert(notification) configureAuthor(notification: notification) - - guard let type = MastodonNotificationType(rawValue: notification.typeRaw) else { - assertionFailure() - return - } - - switch type { + + switch notification.entity.type { case .follow: setAuthorContainerBottomPaddingViewDisplay() case .followRequest: @@ -63,7 +62,7 @@ extension NotificationView { } extension NotificationView { - private func configureAuthor(notification: Notification) { + private func configureAuthor(notification: MastodonNotification) { let author = notification.account // author avatar @@ -98,19 +97,18 @@ extension NotificationView { .assign(to: \.authorUsername, on: viewModel) .store(in: &disposeBag) // timestamp - viewModel.timestamp = notification.createAt + viewModel.timestamp = notification.entity.createdAt - viewModel.visibility = notification.status?.visibility ?? ._other("") + viewModel.visibility = notification.entity.status?.mastodonVisibility ?? ._other("") // notification type indicator - Publishers.CombineLatest3( - notification.publisher(for: \.typeRaw), + Publishers.CombineLatest( author.publisher(for: \.displayName), author.publisher(for: \.emojis) ) - .sink { [weak self] typeRaw, _, emojis in + .sink { [weak self] _, emojis in guard let self = self else { return } - guard let type = MastodonNotificationType(rawValue: typeRaw) else { + guard let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) else { self.viewModel.notificationIndicatorText = nil return } @@ -205,13 +203,8 @@ extension NotificationView { .store(in: &disposeBag) // follow request state - notification.publisher(for: \.followRequestState) - .assign(to: \.followRequestState, on: viewModel) - .store(in: &disposeBag) - - notification.publisher(for: \.transientFollowRequestState) - .assign(to: \.transientFollowRequestState, on: viewModel) - .store(in: &disposeBag) + viewModel.followRequestState = notification.followRequestState + viewModel.transientFollowRequestState = notification.transientFollowRequestState // Following author.publisher(for: \.followingBy) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift index c3455fd0e..ce3fd232d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift @@ -6,7 +6,7 @@ // import UIKit -import CoreDataStack +import MastodonSDK extension StatusTableViewCell { final class ViewModel { @@ -17,8 +17,8 @@ extension StatusTableViewCell { } enum Value { - case feed(Feed) - case status(Status) + case feed(MastodonFeed) + case status(MastodonStatus) } } } @@ -38,13 +38,7 @@ extension StatusTableViewCell { switch viewModel.value { case .feed(let feed): statusView.configure(feed: feed) - - feed.publisher(for: \.hasMore) - .sink { [weak self] hasMore in - guard let self = self else { return } - self.separatorLine.isHidden = hasMore - } - .store(in: &disposeBag) + self.separatorLine.isHidden = feed.hasMore case .status(let status): statusView.configure(status: status) diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift index 568552c16..7c363c916 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusThreadRootTableViewCell+ViewModel.swift @@ -6,7 +6,7 @@ // import UIKit -import CoreDataStack +import MastodonSDK extension StatusThreadRootTableViewCell { final class ViewModel { @@ -17,7 +17,7 @@ extension StatusThreadRootTableViewCell { } enum Value { - case status(Status) + case status(MastodonStatus) } } } diff --git a/Mastodon/Scene/Thread/CachedThreadViewModel.swift b/Mastodon/Scene/Thread/CachedThreadViewModel.swift index 00c29e157..9301d1876 100644 --- a/Mastodon/Scene/Thread/CachedThreadViewModel.swift +++ b/Mastodon/Scene/Thread/CachedThreadViewModel.swift @@ -6,12 +6,12 @@ // import Foundation -import CoreDataStack +import MastodonSDK import MastodonCore final class CachedThreadViewModel: ThreadViewModel { - init(context: AppContext, authContext: AuthContext, status: Status) { - let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID)) + init(context: AppContext, authContext: AuthContext, status: MastodonStatus) { + let threadContext = StatusItem.Thread.Context(status: status) super.init( context: context, authContext: authContext, diff --git a/Mastodon/Scene/Thread/Edit History/StatusEditHistoryTableViewCell.swift b/Mastodon/Scene/Thread/Edit History/StatusEditHistoryTableViewCell.swift index 3485cfeab..9200b9189 100644 --- a/Mastodon/Scene/Thread/Edit History/StatusEditHistoryTableViewCell.swift +++ b/Mastodon/Scene/Thread/Edit History/StatusEditHistoryTableViewCell.swift @@ -72,7 +72,7 @@ class StatusEditHistoryTableViewCell: UITableViewCell { NSLayoutConstraint.activate(constraints) } - func configure(status: Status, statusEdit: Mastodon.Entity.StatusEdit, dateText: String) { + func configure(status: MastodonStatus, statusEdit: Mastodon.Entity.StatusEdit, dateText: String) { dateLabel.text = dateText statusHistoryView.statusView.configure(status: status, statusEdit: statusEdit) } diff --git a/Mastodon/Scene/Thread/Edit History/StatusEditHistoryViewModel.swift b/Mastodon/Scene/Thread/Edit History/StatusEditHistoryViewModel.swift index 525f7e72a..3a16a96ce 100644 --- a/Mastodon/Scene/Thread/Edit History/StatusEditHistoryViewModel.swift +++ b/Mastodon/Scene/Thread/Edit History/StatusEditHistoryViewModel.swift @@ -8,7 +8,7 @@ import UIKit import MastodonSDK struct StatusEditHistoryViewModel { - let status: Status + let status: MastodonStatus let edits: [Mastodon.Entity.StatusEdit] let appContext: AppContext diff --git a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift index ad69791b2..8fb717cd1 100644 --- a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift +++ b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift @@ -20,7 +20,7 @@ final class MastodonStatusThreadViewModel { // input let context: AppContext - @Published private(set) var deletedObjectIDs: Set = Set() + @Published private(set) var deletedObjectIDs: Set = Set() // output @Published var __ancestors: [StatusItem] = [] @@ -41,7 +41,7 @@ final class MastodonStatusThreadViewModel { let newItems = items.filter { item in switch item { case .thread(let thread): - return !deletedObjectIDs.contains(thread.record.objectID) + return !deletedObjectIDs.contains(thread.record.id) default: assertionFailure() return false @@ -60,7 +60,7 @@ final class MastodonStatusThreadViewModel { let newItems = items.filter { item in switch item { case .thread(let thread): - return !deletedObjectIDs.contains(thread.record.objectID) + return !deletedObjectIDs.contains(thread.record.id) default: assertionFailure() return false @@ -94,19 +94,20 @@ extension MastodonStatusThreadViewModel { } var newItems: [StatusItem] = [] - for (i, node) in nodes.enumerated() { - guard let status = dictionary[node.statusID] else { continue } - let isLast = i == nodes.count - 1 - - let record = ManagedObjectRecord(objectID: status.objectID) - let context = StatusItem.Thread.Context( - status: record, - displayUpperConversationLink: !isLast, - displayBottomConversationLink: true - ) - let item = StatusItem.thread(.leaf(context: context)) - newItems.append(item) - } + #warning("Potentially this can be removed and replaced by native threading logic") +// for (i, node) in nodes.enumerated() { +// guard let status = dictionary[node.statusID] else { continue } +// let isLast = i == nodes.count - 1 +// +// let record = ManagedObjectRecord(objectID: status.objectID) +// let context = StatusItem.Thread.Context( +// status: record, +// displayUpperConversationLink: !isLast, +// displayBottomConversationLink: true +// ) +// let item = StatusItem.thread(.leaf(context: context)) +// newItems.append(item) +// } let items = self.__ancestors + newItems self.__ancestors = items @@ -132,31 +133,32 @@ extension MastodonStatusThreadViewModel { } var newItems: [StatusItem] = [] - for node in nodes { - guard let status = dictionary[node.statusID] else { continue } - // first tier - let record = ManagedObjectRecord(objectID: status.objectID) - let context = StatusItem.Thread.Context( - status: record - ) - let item = StatusItem.thread(.leaf(context: context)) - newItems.append(item) - - // second tier - if let child = node.children.first { - guard let secondaryStatus = dictionary[child.statusID] else { continue } - let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) - let secondaryContext = StatusItem.Thread.Context( - status: secondaryRecord, - displayUpperConversationLink: true - ) - let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) - newItems.append(secondaryItem) - - // update first tier context - context.displayBottomConversationLink = true - } - } +#warning("Potentially this can be removed and replaced by native threading logic") +// for node in nodes { +// guard let status = dictionary[node.statusID] else { continue } +// // first tier +// let record = ManagedObjectRecord(objectID: status.objectID) +// let context = StatusItem.Thread.Context( +// status: record +// ) +// let item = StatusItem.thread(.leaf(context: context)) +// newItems.append(item) +// +// // second tier +// if let child = node.children.first { +// guard let secondaryStatus = dictionary[child.statusID] else { continue } +// let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) +// let secondaryContext = StatusItem.Thread.Context( +// status: secondaryRecord, +// displayUpperConversationLink: true +// ) +// let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) +// newItems.append(secondaryItem) +// +// // update first tier context +// context.displayBottomConversationLink = true +// } +// } var items = self.__descendants for item in newItems { @@ -262,12 +264,3 @@ extension MastodonStatusThreadViewModel.Node { } -extension MastodonStatusThreadViewModel { - func delete(objectIDs: [NSManagedObjectID]) { - var set = deletedObjectIDs - for objectID in objectIDs { - set.insert(objectID) - } - self.deletedObjectIDs = set - } -} diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift index 696e10492..1585b524b 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -30,15 +30,7 @@ final class RemoteThreadViewModel: ThreadViewModel { authenticationBox: authContext.mastodonAuthenticationBox ) - let managedObjectContext = context.managedObjectContext - let request = Status.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = Status.predicate(domain: domain, id: response.value.id) - guard let status = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID)) + let threadContext = StatusItem.Thread.Context(status: .fromEntity(response.value)) self.root = .root(context: threadContext) } // end Task @@ -62,17 +54,9 @@ final class RemoteThreadViewModel: ThreadViewModel { authenticationBox: authContext.mastodonAuthenticationBox ) - guard let statusID = response.value.status?.id else { return } + guard let status = response.value.status else { return } - let managedObjectContext = context.managedObjectContext - let request = Status.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = Status.predicate(domain: domain, id: statusID) - guard let status = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID)) + let threadContext = StatusItem.Thread.Context(status: .fromEntity(status)) self.root = .root(context: threadContext) } // end Task } diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index aa5f33cec..9ab629ad9 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -38,8 +38,7 @@ extension ThreadViewModel { snapshot.appendSections([.main]) if let root = self.root { if case let .root(threadContext) = root, - let status = threadContext.status.object(in: context.managedObjectContext), - status.inReplyToID != nil + threadContext.status.entity.inReplyToID != nil { snapshot.appendItems([.topLoader], toSection: .main) } @@ -81,8 +80,7 @@ extension ThreadViewModel { // top loader let _hasReplyTo: Bool? = try? await self.context.managedObjectContext.perform { guard case let .root(threadContext) = root else { return nil } - guard let status = threadContext.status.object(in: self.context.managedObjectContext) else { return nil } - return status.inReplyToID != nil + return threadContext.status.entity.inReplyToID != nil } if let hasReplyTo = _hasReplyTo, hasReplyTo { let state = self.loadThreadStateMachine.currentState diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index 69dc73e48..b4bf03d93 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -55,40 +55,25 @@ class ThreadViewModel { self.root = optionalRoot self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) // end init - - ManagedObjectObserver.observe(context: context.managedObjectContext) - .sink(receiveCompletion: { completion in - // do nohting - }, receiveValue: { [weak self] changes in - guard let self = self else { return } - - let objectIDs: [NSManagedObjectID] = changes.changeTypes.compactMap { changeType in - guard case let .delete(object) = changeType else { return nil } - return object.objectID - } - - self.delete(objectIDs: objectIDs) - }) - .store(in: &disposeBag) - + $root .receive(on: DispatchQueue.main) .sink { [weak self] root in guard let self = self else { return } guard case let .root(threadContext) = root else { return } - guard let status = threadContext.status.object(in: self.context.managedObjectContext) else { return } + let status = threadContext.status // bind threadContext self.threadContext = .init( - domain: status.domain, + domain: authContext.mastodonAuthenticationBox.domain, //status.domain, statusID: status.id, - replyToID: status.inReplyToID + replyToID: status.entity.inReplyToID ) // bind titleView self.navigationBarTitle = { - let title = L10n.Scene.Thread.title(status.author.displayNameWithFallback) - let content = MastodonContent(content: title, emojis: status.author.emojis.asDictionary) + let title = L10n.Scene.Thread.title(status.entity.account.displayNameWithFallback) + let content = MastodonContent(content: title, emojis: status.entity.account.emojis?.asDictionary ?? [:]) return try? MastodonMetaContent.convert(document: content) }() } @@ -116,16 +101,3 @@ extension ThreadViewModel { } } - -extension ThreadViewModel { - func delete(objectIDs: [NSManagedObjectID]) { - if let root = self.root, - case let .root(threadContext) = root, - objectIDs.contains(threadContext.status.objectID) - { - self.root = nil - } - - self.mastodonStatusThreadViewModel.delete(objectIDs: objectIDs) - } -} diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 682c83815..c578ce404 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -9,87 +9,11 @@ import Foundation import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK -final public class FeedFetchedResultsController: NSObject { - - private enum Constants { - static let defaultFetchLimit = 100 - } - - var disposeBag = Set() - - private let fetchedResultsController: NSFetchedResultsController - - public var managedObjectContext: NSManagedObjectContext { - fetchedResultsController.managedObjectContext - } - - // input - @Published public var predicate = Feed.predicate(kind: .none, acct: .none) - - // output - private let _objectIDs = PassthroughSubject<[NSManagedObjectID], Never>() - @Published public var records: [ManagedObjectRecord] = [] +final public class FeedFetchedResultsController { - public func fetchNextBatch() { - fetchedResultsController.fetchRequest.fetchLimit += Constants.defaultFetchLimit - try? fetchedResultsController.performFetch() - } - - public init(managedObjectContext: NSManagedObjectContext) { - self.fetchedResultsController = { - let fetchRequest = Feed.sortedFetchRequest - // make sure initial query return empty results - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.shouldRefreshRefetchedObjects = true - fetchRequest.fetchBatchSize = 15 - fetchRequest.fetchLimit = Constants.defaultFetchLimit - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - return controller - }() - super.init() - - // debounce output to prevent UI update issues - _objectIDs - .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) - .map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } } - .assign(to: &$records) - - fetchedResultsController.delegate = self - - $predicate - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] predicate in - guard let self = self else { return } - self.fetchedResultsController.fetchRequest.predicate = predicate - do { - try self.fetchedResultsController.performFetch() - } catch { - assertionFailure(error.localizedDescription) - } - } - .store(in: &disposeBag) - } + @Published public var records: [MastodonFeed] = [] + + public init() {} } - -// MARK: - NSFetchedResultsControllerDelegate -extension FeedFetchedResultsController: NSFetchedResultsControllerDelegate { - public func controller( - _ controller: NSFetchedResultsController, - didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference - ) { - let snapshot = snapshot as NSDiffableDataSourceSnapshot - self._objectIDs.send(snapshot.itemIdentifiers) - } -} - diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 1bc5426c6..d34cd1add 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -11,93 +11,27 @@ import CoreData import CoreDataStack import MastodonSDK -public final class StatusFetchedResultsController: NSObject { - - var disposeBag = Set() - - let fetchedResultsController: NSFetchedResultsController - - // input - @Published public var domain: String? = nil - @Published public var statusIDs: [Mastodon.Entity.Status.ID] = [] +public final class StatusFetchedResultsController { + @MainActor + @Published public private(set) var records: [MastodonStatus] = [] - // output - let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - @Published public private(set) var records: [ManagedObjectRecord] = [] - - public init(managedObjectContext: NSManagedObjectContext, domain: String?, additionalTweetPredicate: NSPredicate?) { - self.domain = domain ?? "" - self.fetchedResultsController = { - let fetchRequest = Status.sortedFetchRequest - fetchRequest.predicate = Status.predicate(domain: domain ?? "", ids: []) - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - return controller - }() - super.init() - - // debounce output to prevent UI update issues - _objectIDs - .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) - .map { objectIDs in objectIDs.map { ManagedObjectRecord(objectID: $0) } } - .assign(to: &$records) - - fetchedResultsController.delegate = self - - Publishers.CombineLatest( - self.$domain.removeDuplicates(), - self.$statusIDs.removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] domain, ids in - guard let self = self else { return } - var predicates = [Status.predicate(domain: domain ?? "", ids: ids)] - if let additionalPredicate = additionalTweetPredicate { - predicates.append(additionalPredicate) - } - self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) - do { - try self.fetchedResultsController.performFetch() - } catch { - assertionFailure(error.localizedDescription) - } - } - .store(in: &disposeBag) + @MainActor + public init(records: [MastodonStatus] = []) { + self.records = records } -} - -extension StatusFetchedResultsController { - - public func append(statusIDs: [Mastodon.Entity.Status.ID]) { - var result = self.statusIDs - for statusID in statusIDs where !result.contains(statusID) { - result.append(statusID) - } - self.statusIDs = result + @MainActor + public func reset() { + records = [] } -} - -// MARK: - NSFetchedResultsControllerDelegate -extension StatusFetchedResultsController: NSFetchedResultsControllerDelegate { - public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - let indexes = statusIDs - let objects = fetchedResultsController.fetchedObjects ?? [] - - let items: [NSManagedObjectID] = objects - .compactMap { object in - indexes.firstIndex(of: object.id).map { index in (index, object) } - } - .sorted { $0.0 < $1.0 } - .map { $0.1.objectID } - self._objectIDs.value = items + @MainActor + public func setRecords(_ records: [MastodonStatus]) { + self.records = records + } + + @MainActor + public func appendRecords(_ records: [MastodonStatus]) { + self.records += records } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift index 8c4e48417..4f31c91bc 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift @@ -19,26 +19,26 @@ extension APIService { } public func bookmark( - record: ManagedObjectRecord, + record: MastodonStatus, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { let managedObjectContext = backgroundManagedObjectContext - + // update bookmark state and retrieve bookmark context let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges { let authentication = authenticationBox.authentication guard - let _status = record.object(in: managedObjectContext), let me = authentication.user(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } - + + let _status = record.entity let status = _status.reblog ?? _status - let isBookmarked = status.bookmarkedBy.contains(me) - status.update(bookmarked: !isBookmarked, by: me) + let isBookmarked = status.bookmarked == true + let context = MastodonBookmarkContext( statusID: status.id, isBookmarked: isBookmarked @@ -60,38 +60,12 @@ extension APIService { } catch { result = .failure(error) } + + let response = try result.get() // update bookmark state - try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let _status = record.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { return } - - let status = _status.reblog ?? _status - - switch result { - case .success(let response): - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: authenticationBox.domain, - entity: response.value, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - status.update(bookmarked: bookmarkContext.isBookmarked, by: me) - } - } + record.entity = response.value - let response = try result.get() return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index ec44afa93..e5bd28231 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -14,13 +14,13 @@ import CoreDataStack extension APIService { private struct MastodonFavoriteContext { - let statusID: Status.ID + let statusID: MastodonStatus.ID let isFavorited: Bool let favoritedCount: Int64 } public func favorite( - record: ManagedObjectRecord, + status: MastodonStatus, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { @@ -31,18 +31,15 @@ extension APIService { let authentication = authenticationBox.authentication guard - let _status = record.object(in: managedObjectContext), let me = authentication.user(in: managedObjectContext) else { throw APIError.implicit(.badRequest) } - let status = _status.reblog ?? _status - let isFavorited = status.favouritedBy.contains(me) - let favoritedCount = status.favouritesCount - let favoriteCount = isFavorited ? favoritedCount - 1 : favoritedCount + 1 - status.update(liked: !isFavorited, by: me) - status.update(favouritesCount: favoriteCount) + let _status = status.reblog ?? status + let isFavorited = status.entity.favourited == true + let favoritedCount = Int64(status.entity.favouritesCount) + let context = MastodonFavoriteContext( statusID: status.id, isFavorited: isFavorited, @@ -66,40 +63,6 @@ extension APIService { result = .failure(error) } - // update like state - try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let _status = record.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { return } - - let status = _status.reblog ?? _status - - switch result { - case .success(let response): - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: authenticationBox.domain, - entity: response.value, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - if favoriteContext.isFavorited { - status.update(favouritesCount: max(0, status.favouritesCount - 1)) // undo API return count has delay. Needs -1 local - } - case .failure: - // rollback - status.update(liked: favoriteContext.isFavorited, by: me) - status.update(favouritesCount: favoriteContext.favoritedCount) - } - } - let response = try result.get() return response } @@ -152,19 +115,11 @@ extension APIService { extension APIService { public func favoritedBy( - status: ManagedObjectRecord, + status: MastodonStatus, query: Mastodon.API.Statuses.FavoriteByQuery, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { - let managedObjectContext = backgroundManagedObjectContext - let _statusID: Status.ID? = try? await managedObjectContext.perform { - guard let _status = status.object(in: managedObjectContext) else { return nil } - let status = _status.reblog ?? _status - return status.id - } - guard let statusID = _statusID else { - throw APIError.implicit(.badRequest) - } + let statusID: String = status.reblog?.id ?? status.id let response = try await Mastodon.API.Statuses.favoriteBy( session: session, @@ -173,21 +128,7 @@ extension APIService { query: query, authorization: authenticationBox.userAuthorization ).singleOutput() - - try await managedObjectContext.performChanges { - for entity in response.value { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: .init( - domain: authenticationBox.domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - } // end for … in - } - + return response } // end func } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index d3d5e1c15..6bcc8dab1 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -20,7 +20,7 @@ extension APIService { } public func reblog( - record: ManagedObjectRecord, + status: MastodonStatus, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { let managedObjectContext = backgroundManagedObjectContext @@ -30,19 +30,16 @@ extension APIService { let authentication = authenticationBox.authentication guard - let me = authentication.user(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) + let me = authentication.user(in: managedObjectContext) else { return nil } - let status = _status.reblog ?? _status - let isReblogged = status.rebloggedBy.contains(me) - let rebloggedCount = status.reblogsCount - let reblogCount = isReblogged ? rebloggedCount - 1 : rebloggedCount + 1 - status.update(reblogged: !isReblogged, by: me) - status.update(reblogsCount: Int64(max(0, reblogCount))) + let _status = status.reblog ?? status + let isReblogged = _status.entity.reblogged == true + let rebloggedCount = Int64(_status.entity.reblogsCount) + let reblogContext = MastodonReblogContext( - statusID: status.id, - isReblogged: isReblogged, + statusID: _status.id, + isReblogged: !isReblogged, rebloggedCount: rebloggedCount ) return reblogContext @@ -65,41 +62,7 @@ extension APIService { } catch { result = .failure(error) } - - // update repost state - try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let me = authentication.user(in: managedObjectContext), - let _status = record.object(in: managedObjectContext) - else { return } - - let status = _status.reblog ?? _status - - switch result { - case .success(let response): - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: authentication.domain, - entity: response.value, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - if reblogContext.isReblogged { - status.update(reblogsCount: max(0, status.reblogsCount - 1)) // undo API return count has delay. Needs -1 local - } - case .failure: - // rollback - status.update(reblogged: reblogContext.isReblogged, by: me) - status.update(reblogsCount: reblogContext.rebloggedCount) - } - } - + let response = try result.get() return response } @@ -108,19 +71,12 @@ extension APIService { extension APIService { public func rebloggedBy( - status: ManagedObjectRecord, + status: MastodonStatus, query: Mastodon.API.Statuses.RebloggedByQuery, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { let managedObjectContext = backgroundManagedObjectContext - let _statusID: Status.ID? = try? await managedObjectContext.perform { - guard let _status = status.object(in: managedObjectContext) else { return nil } - let status = _status.reblog ?? _status - return status.id - } - guard let statusID = _statusID else { - throw APIError.implicit(.badRequest) - } + let statusID: Status.ID = status.reblog?.id ?? status.id let response = try await Mastodon.API.Statuses.rebloggedBy( session: session, @@ -130,6 +86,7 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() + #warning("Is this still required?") try await managedObjectContext.performChanges { for entity in response.value { _ = Persistence.MastodonUser.createOrMerge( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift index b67b79349..6e1d6055d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -47,14 +47,14 @@ extension APIService { } public func deleteStatus( - status: ManagedObjectRecord, + status: MastodonStatus, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { let authorization = authenticationBox.userAuthorization let managedObjectContext = backgroundManagedObjectContext let _query: Mastodon.API.Statuses.DeleteStatusQuery? = try? await managedObjectContext.perform { - guard let _status = status.object(in: managedObjectContext) else { return nil } + let _status = status.entity let status = _status.reblog ?? _status return Mastodon.API.Statuses.DeleteStatusQuery(id: status.id) } @@ -68,12 +68,7 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - try await managedObjectContext.performChanges { - guard let status = status.object(in: managedObjectContext) else { return } - managedObjectContext.delete(status) - } - + return response } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 9e37770f9..f189a131e 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -104,6 +104,15 @@ extension Mastodon.Entity.Account: Equatable { //MARK: - Convenience extension Mastodon.Entity.Account { + public var acctWithDomain: String { + if !acct.contains("@") { + // Safe concat due to username cannot contains "@" + return username + "@" + (domain ?? "") + } else { + return acct + } + } + public func acctWithDomainIfMissing(_ localDomain: String) -> String { guard acct.contains("@") else { return "\(acct)@\(localDomain)" diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Card.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Card.swift index 69b759045..6e022b7af 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Card.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Card.swift @@ -83,3 +83,13 @@ extension Mastodon.Entity.Card { } } } + +extension Mastodon.Entity.Card: Hashable { + public static func == (lhs: Mastodon.Entity.Card, rhs: Mastodon.Entity.Card) -> Bool { + lhs.url == rhs.url + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(url) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift index 7b500089e..d6a2f038d 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift @@ -88,3 +88,13 @@ extension Mastodon.Entity.Notification { } } } + +extension Mastodon.Entity.Notification: Hashable { + public static func == (lhs: Mastodon.Entity.Notification, rhs: Mastodon.Entity.Notification) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index 66b9667a2..b56b9067d 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -133,3 +133,14 @@ extension Mastodon.Entity.Status { } } } + +extension Mastodon.Entity.Status: Hashable { + public static func == (lhs: Mastodon.Entity.Status, rhs: Mastodon.Entity.Status) -> Bool { + lhs.uri == rhs.uri && lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(uri) + hasher.combine(id) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift new file mode 100644 index 000000000..463909e72 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -0,0 +1,56 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import CoreDataStack + +public final class MastodonFeed { + public var hasMore: Bool = false + public var isLoadingMore: Bool = false + + public let status: MastodonStatus? + public let notification: Mastodon.Entity.Notification? + + public let kind: Feed.Kind + + init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, kind: Feed.Kind) { + self.hasMore = hasMore + self.isLoadingMore = isLoadingMore + self.status = status + self.notification = notification + self.kind = kind + } +} + +public extension MastodonFeed { + static func fromStatus(_ status: MastodonStatus, kind: Feed.Kind) -> MastodonFeed { + MastodonFeed( + hasMore: false, + isLoadingMore: false, + status: status, + notification: nil, + kind: kind + ) + } + + static func fromNotification(_ notification: Mastodon.Entity.Notification, kind: Feed.Kind) -> MastodonFeed { + MastodonFeed( + hasMore: false, + isLoadingMore: false, + status: nil, + notification: notification, + kind: kind + ) + } +} + +extension MastodonFeed: Hashable { + public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool { + lhs.status?.id == rhs.status?.id || lhs.notification?.id == rhs.notification?.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(status) + hasher.combine(notification) + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift new file mode 100644 index 000000000..4cffc393f --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift @@ -0,0 +1,45 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import CoreDataStack + +public final class MastodonNotification { + public let entity: Mastodon.Entity.Notification + + public var id: Mastodon.Entity.Notification.ID { + entity.id + } + + public let account: MastodonUser + public let status: MastodonStatus? + public let feeds: [MastodonFeed] + + public var followRequestState: MastodonFollowRequestState = .init(state: .none) + public var transientFollowRequestState: MastodonFollowRequestState = .init(state: .none) + + public init(entity: Mastodon.Entity.Notification, account: MastodonUser, status: MastodonStatus?, feeds: [MastodonFeed]) { + self.entity = entity + self.account = account + self.status = status + self.feeds = feeds + } +} + +public extension MastodonNotification { + static func fromEntity(_ entity: Mastodon.Entity.Notification, using managedObjectContext: NSManagedObjectContext) -> MastodonNotification? { + guard let user = MastodonUser.fetch(in: managedObjectContext, configurationBlock: { request in + request.predicate = MastodonUser.predicate(domain: entity.account.domain ?? "", id: entity.account.id) + }).first else { return nil } + return MastodonNotification(entity: entity, account: user, status: entity.status.map(MastodonStatus.fromEntity), feeds: []) + } +} + +extension MastodonNotification: Hashable { + public static func == (lhs: MastodonNotification, rhs: MastodonNotification) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift new file mode 100644 index 000000000..24dbfa3a7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift @@ -0,0 +1,54 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import Combine +import CoreDataStack + +public final class MastodonStatus: ObservableObject { + public typealias ID = Mastodon.Entity.Status.ID + + @Published public var entity: Mastodon.Entity.Status + @Published public private(set) var reblog: MastodonStatus? + + @Published public var isSensitiveToggled: Bool = false + + init(entity: Mastodon.Entity.Status, isSensitiveToggled: Bool) { + self.entity = entity + self.isSensitiveToggled = isSensitiveToggled + + if let reblog = entity.reblog { + self.reblog = MastodonStatus.fromEntity(reblog) + } else { + self.reblog = nil + } + } + + public var id: ID { + entity.id + } +} + +extension MastodonStatus { + public static func fromEntity(_ entity: Mastodon.Entity.Status) -> MastodonStatus { + return MastodonStatus(entity: entity, isSensitiveToggled: false) + } +} + +extension MastodonStatus: Hashable { + public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool { + lhs.entity.id == rhs.entity.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(entity) + hasher.combine(isSensitiveToggled) + } +} + +public extension Mastodon.Entity.Status { + var mastodonVisibility: MastodonVisibility? { + guard let visibility = visibility?.rawValue else { return nil } + return MastodonVisibility(rawValue: visibility) + } +} + diff --git a/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift b/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift index 5977cba12..92b94ee61 100644 --- a/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift +++ b/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift @@ -2,6 +2,7 @@ import Foundation import CoreDataStack +import MastodonSDK public protocol StatusCompatible { var reblog: Status? { get } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift index 9f036643b..a562e26c4 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -60,10 +60,8 @@ extension ComposeContentViewModel { cell.statusView.frame.size.width = tableView.frame.width // configure status - context.managedObjectContext.performAndWait { - guard let replyTo = status.object(in: context.managedObjectContext) else { return } - cell.statusView.configure(status: replyTo) - } + cell.statusView.configure(status: status) + } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index cd4b67780..cac5b71da 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -23,7 +23,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { public enum ComposeContext { case composeStatus - case editStatus(status: Status, statusSource: Mastodon.Entity.StatusSource) + case editStatus(status: MastodonStatus, statusSource: Mastodon.Entity.StatusSource) } var disposeBag = Set() @@ -163,24 +163,18 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { }() // set visibility for reply post if case .reply(let record) = destination { - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { - assertionFailure() - return - } - let repliedStatusVisibility = status.visibility - switch repliedStatusVisibility { - case .public, .unlisted: - // keep default - break - case .private: - visibility = .private - case .direct: - visibility = .direct - case ._other: - assertionFailure() - break - } + let repliedStatusVisibility = record.entity.visibility + switch repliedStatusVisibility { + case .public, .unlisted: + // keep default + break + case .private: + visibility = .private + case .direct: + visibility = .direct + case ._other, .none: + assertionFailure() + break } } return visibility @@ -189,26 +183,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel( for: authContext.mastodonAuthenticationBox.domain ) - - if case let ComposeContext.editStatus(status, _) = composeContext { - if status.isContentSensitive { - isContentWarningActive = true - contentWarning = status.spoilerText ?? "" - } - if let poll = status.poll { - isPollActive = !poll.expired - pollMultipleConfigurationOption = poll.multiple - if let pollExpiresAt = poll.expiresAt { - pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt) - } - pollOptions = poll.options.sortedByIndex().map { - let option = PollComposeItem.Option() - option.text = $0.title - return option - } - } - } - + let recentLanguages = context.settingService.currentSetting.value?.recentLanguages ?? [] self.recentLanguages = recentLanguages self.language = recentLanguages.first ?? Locale.current.language.languageCode?.identifier ?? "en" @@ -220,17 +195,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { switch destination { case .reply(let record): context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { - assertionFailure() - return - } + let status = record.entity let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) var mentionAccts: [String] = [] - if author?.id != status.author.id { - mentionAccts.append("@" + status.author.acct) + if author?.id != status.account.id { + mentionAccts.append("@" + status.account.acct) } - let mentions = status.mentions + let mentions = status.mentions ?? [] .filter { author?.id != $0.id } for mention in mentions { let acct = "@" + mention.acct @@ -288,11 +260,11 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { case .composeStatus: self.isVisibilityButtonEnabled = true case let .editStatus(status, _): - if let visibility = Mastodon.Entity.Status.Visibility(rawValue: status.visibility.rawValue) { + if let visibility = status.entity.visibility { self.visibility = visibility } self.isVisibilityButtonEnabled = false - self.attachmentViewModels = status.attachments.compactMap { + self.attachmentViewModels = status.entity.mastodonAttachments.compactMap { guard let assetURL = $0.assetURL, let url = URL(string: assetURL) else { return nil } let attachmentViewModel = AttachmentViewModel( api: context.apiService, @@ -306,6 +278,27 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } } + if case let ComposeContext.editStatus(status, _) = composeContext { + if status.entity.sensitive == true { + isContentWarningActive = true + contentWarning = status.entity.spoilerText ?? "" + } + Task { + if let poll = await status.getPoll(in: context.managedObjectContext) { + isPollActive = !poll.expired + pollMultipleConfigurationOption = poll.multiple + if let pollExpiresAt = poll.expiresAt { + pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt) + } + pollOptions = poll.options.sortedByIndex().map { + let option = PollComposeItem.Option() + option.text = $0.title + return option + } + } + } + } + bind() } @@ -503,7 +496,7 @@ extension ComposeContentViewModel { extension ComposeContentViewModel { public enum Destination { case topLevel - case reply(parent: ManagedObjectRecord) + case reply(parent: MastodonStatus) } public enum ScrollViewState { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index dfa7d3ef7..9db9faed9 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -19,7 +19,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { // author public let author: ManagedObjectRecord // refer - public let replyTo: ManagedObjectRecord? + public let replyTo: MastodonStatus? // content warning public let isContentWarningComposing: Bool public let contentWarning: String @@ -48,7 +48,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { public init( author: ManagedObjectRecord, - replyTo: ManagedObjectRecord?, + replyTo: MastodonStatus?, isContentWarningComposing: Bool, contentWarning: String, content: String, @@ -162,7 +162,7 @@ extension MastodonStatusPublisher: StatusPublisher { return self.pollExpireConfigurationOption.seconds }() let inReplyToID: Mastodon.Entity.Status.ID? = try await api.backgroundManagedObjectContext.perform { - guard let replyTo = self.replyTo?.object(in: api.backgroundManagedObjectContext) else { return nil } + guard let replyTo = self.replyTo else { return nil } return replyTo.id } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index e3bed16ae..67372f544 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -180,7 +180,7 @@ extension MediaView.Configuration { } extension MediaView { - public static func configuration(status: StatusCompatible) -> [MediaView.Configuration] { + public static func configuration(status: MastodonStatus) -> [MediaView.Configuration] { func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo { MediaView.Configuration.VideoInfo( aspectRadio: attachment.size, @@ -191,8 +191,8 @@ extension MediaView { ) } - let status: StatusCompatible = status.reblog ?? status - let attachments = status.attachments +// let status: StatusCompatible = status.reblog ?? status + let attachments = status.entity.mastodonAttachments let configurations = attachments.enumerated().map { (idx, attachment) -> MediaView.Configuration in let configuration: MediaView.Configuration = { switch attachment.kind { @@ -236,7 +236,7 @@ extension MediaView { }() configuration.load() - configuration.isReveal = status.isMediaSensitive ? status.isSensitiveToggled : true + configuration.isReveal = status.entity.sensitive == true ? status.isSensitiveToggled : true return configuration } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index b51011e54..ad59df742 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -19,7 +19,7 @@ import CoreDataStack extension NotificationView { public final class ViewModel: ObservableObject { public var disposeBag = Set() - public var objects = Set() + public var objects = Set() @Published public var context: AppContext? @Published public var authContext: AuthContext? diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 5fead2f3c..14a87c6ea 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -13,6 +13,7 @@ import MastodonLocalization import CoreDataStack import UIKit import WebKit +import MastodonSDK public protocol StatusCardControlDelegate: AnyObject { func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL) @@ -133,20 +134,24 @@ public final class StatusCardControl: UIControl { fatalError("init(coder:) has not been implemented") } - public func configure(card: Card) { + public func configure(card: Mastodon.Entity.Card) { let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines) - if let host = card.url?.host { + let url = URL(string: card.url) + if let host = url?.host { accessibilityLabel = "\(title) \(host)" } else { accessibilityLabel = title } titleLabel.text = title - linkLabel.text = card.url?.host + linkLabel.text = url?.host imageView.contentMode = .center imageView.sd_setImage( - with: card.imageURL, + with: { + guard let image = card.image else { return nil } + return URL(string: image) + }(), placeholderImage: icon(for: card.layout) ) { [weak self] image, _, _, _ in if image != nil { @@ -333,6 +338,18 @@ private extension Card { } } +private extension Mastodon.Entity.Card { + var layout: StatusCardControl.Layout { + var aspectRatio = CGFloat(width ?? 1) / CGFloat(height ?? 1) + if !aspectRatio.isFinite { + aspectRatio = 1 + } + return (abs(aspectRatio - 1) < 0.05 || image == nil) && html == nil + ? .compact + : .large(aspectRatio: aspectRatio) + } +} + private extension UILayoutPriority { static let zero = UILayoutPriority(rawValue: 0) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 5257fa483..647c31ba2 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -19,7 +19,7 @@ extension StatusView { static let statusFilterWorkingQueue = DispatchQueue(label: "StatusFilterWorkingQueue") - public func configure(feed: Feed) { + public func configure(feed: MastodonFeed) { switch feed.kind { case .home: guard let status = feed.status else { @@ -40,18 +40,12 @@ extension StatusView { extension StatusView { - public func configure(status: Status, statusEdit: Mastodon.Entity.StatusEdit) { - viewModel.objects.insert(status) - if let reblog = status.reblog { - viewModel.objects.insert(reblog) - } - + public func configure(status: MastodonStatus, statusEdit: Mastodon.Entity.StatusEdit) { configureHeader(status: status) - let author = (status.reblog ?? status).author + let author = (status.reblog ?? status).entity.account configureAuthor(author: author) - let timestamp = (status.reblog ?? status).publisher(for: \.createdAt) - configureTimestamp(timestamp: timestamp.eraseToAnyPublisher()) - configureApplicationName(status.application?.name) + configureTimestamp(timestamp: (status.reblog ?? status).entity.createdAt) + configureApplicationName(status.entity.application?.name) configureMedia(status: status) configurePollHistory(statusEdit: statusEdit) configureCard(status: status) @@ -66,18 +60,13 @@ extension StatusView { viewModel.isContentReveal = true } - public func configure(status: Status) { - viewModel.objects.insert(status) - if let reblog = status.reblog { - viewModel.objects.insert(reblog) - } - + public func configure(status: MastodonStatus) { configureHeader(status: status) - let author = (status.reblog ?? status).author + let author = (status.reblog ?? status).entity.account configureAuthor(author: author) - let timestamp = (status.reblog ?? status).publisher(for: \.createdAt) - configureTimestamp(timestamp: timestamp.eraseToAnyPublisher()) - configureApplicationName(status.application?.name) + let timestamp = (status.reblog ?? status).entity.createdAt + configureTimestamp(timestamp: timestamp) + configureApplicationName(status.entity.application?.name) configureContent(status: status) configureMedia(status: status) configurePoll(status: status) @@ -96,14 +85,13 @@ extension StatusView { } extension StatusView { - private func configureHeader(status: Status) { + private func configureHeader(status: MastodonStatus) { if let _ = status.reblog { - Publishers.CombineLatest( - status.author.publisher(for: \.displayName), - status.author.publisher(for: \.emojis) - ) - .map { name, emojis -> StatusView.ViewModel.Header in - let text = L10n.Common.Controls.Status.userReblogged(status.author.displayNameWithFallback) + let name = status.entity.account.displayName + let emojis = status.entity.account.emojis ?? [] + + viewModel.header = { + let text = L10n.Common.Controls.Status.userReblogged(status.entity.account.displayNameWithFallback) let content = MastodonContent(content: text, emojis: emojis.asDictionary) do { let metaContent = try MastodonMetaContent.convert(document: content) @@ -112,12 +100,10 @@ extension StatusView { let metaContent = PlaintextMetaContent(string: name) return .repost(info: .init(header: metaContent)) } - - } - .assign(to: \.header, on: viewModel) - .store(in: &disposeBag) - } else if let _ = status.inReplyToID, - let inReplyToAccountID = status.inReplyToAccountID + }() + + } else if let _ = status.entity.inReplyToID, + let inReplyToAccountID = status.entity.inReplyToAccountID { func createHeader( name: String?, @@ -139,20 +125,31 @@ extension StatusView { return header } - if let replyTo = status.replyTo { + if let inReplyToID = status.entity.inReplyToID { // A. replyTo status exist - let header = createHeader(name: replyTo.author.displayNameWithFallback, emojis: replyTo.author.emojis.asDictionary) - viewModel.header = header + if let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox { + Task { + if let replyTo = try? await Mastodon.API.Statuses.status( + session: .shared, + domain: authenticationBox.domain, + statusID: inReplyToID, + authorization: authenticationBox.userAuthorization + ).singleOutput().value { + let header = createHeader(name: replyTo.account.displayNameWithFallback, emojis: replyTo.account.emojis?.asDictionary ?? [:]) + viewModel.header = header + } + } + } } else { // B. replyTo status not exist - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: status.domain, id: inReplyToAccountID) - if let user = status.managedObjectContext?.safeFetch(request).first { - // B1. replyTo user exist - let header = createHeader(name: user.displayNameWithFallback, emojis: user.emojis.asDictionary) - viewModel.header = header - } else { +// let request = MastodonUser.sortedFetchRequest +// request.predicate = MastodonUser.predicate(domain: status.domain, id: inReplyToAccountID) +// if let user = status.managedObjectContext?.safeFetch(request).first { +// // B1. replyTo user exist +// let header = createHeader(name: user.displayNameWithFallback, emojis: user.emojis.asDictionary) +// viewModel.header = header +// } else { // B2. replyTo user not exist let header = createHeader(name: nil, emojis: nil) viewModel.header = header @@ -178,7 +175,7 @@ extension StatusView { } .store(in: &disposeBag) } // end if let - } // end else B2. +// } // end else B2. } // end else B. } else { @@ -186,90 +183,56 @@ extension StatusView { } } - public func configureAuthor(author: MastodonUser) { + public func configureAuthor(author: Mastodon.Entity.Account) { // author avatar - Publishers.CombineLatest( - author.publisher(for: \.avatar), - UserDefaults.shared.publisher(for: \.preferredStaticAvatar) - ) - .map { _ in author.avatarImageURL() } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) + viewModel.authorAvatarImageURL = author.avatarImageURL() + let emojis = author.emojis?.asDictionary ?? [:] + // author name - Publishers.CombineLatest( - author.publisher(for: \.displayName), - author.publisher(for: \.emojis) - ) - .map { _, emojis in + viewModel.authorName = { do { - let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis.asDictionary) + let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { assertionFailure(error.localizedDescription) return PlaintextMetaContent(string: author.displayNameWithFallback) } - } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) + }() + // author username - author.publisher(for: \.acct) - .map { $0 as String? } - .assign(to: \.authorUsername, on: viewModel) - .store(in: &disposeBag) - // locked - author.publisher(for: \.locked) - .assign(to: \.locked, on: viewModel) - .store(in: &disposeBag) - // isMuting - author.publisher(for: \.mutingBy) - .map { [weak viewModel] mutingBy in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return mutingBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain - }) - } - .assign(to: \.isMuting, on: viewModel) - .store(in: &disposeBag) - // isBlocking - author.publisher(for: \.blockingBy) - .map { [weak viewModel] blockingBy in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return blockingBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain - }) - } - .assign(to: \.isBlocking, on: viewModel) - .store(in: &disposeBag) - // isMyself - Publishers.CombineLatest( - author.publisher(for: \.domain), - author.publisher(for: \.id) - ) - .map { [weak viewModel] domain, id in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return authContext.mastodonAuthenticationBox.domain == domain && authContext.mastodonAuthenticationBox.userID == id - } - .assign(to: \.isMyself, on: viewModel) - .store(in: &disposeBag) + viewModel.authorUsername = author.acct - // Following - author.publisher(for: \.followingBy) - .map { [weak viewModel] followingBy in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return followingBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain - }) + // locked + viewModel.locked = author.locked + + // isMuting, isBlocking, Following + Task { + guard let auth = viewModel.authContext?.mastodonAuthenticationBox else { return } + if let relationship = try? await Mastodon.API.Account.relationships( + session: .shared, + domain: auth.domain, + query: .init(ids: [author.id]), + authorization: auth.userAuthorization + ).singleOutput().value { + guard let rel = relationship.first else { return } + DispatchQueue.main.async { [self] in + viewModel.isMuting = rel.muting ?? false + viewModel.isBlocking = rel.blocking + viewModel.isFollowed = rel.followedBy + } } - .assign(to: \.isFollowed, on: viewModel) - .store(in: &disposeBag) + } + + // isMyself + viewModel.isMyself = { + guard let authContext = viewModel.authContext else { return false } + return authContext.mastodonAuthenticationBox.domain == author.domain && authContext.mastodonAuthenticationBox.userID == author.id + }() + } - private func configureTimestamp(timestamp: AnyPublisher) { + private func configureTimestamp(timestamp: Date) { // timestamp viewModel.timestampFormatter = { (date: Date, isEdited: Bool) in if isEdited { @@ -277,10 +240,7 @@ extension StatusView { } return date.localizedSlowedTimeAgoSinceNow } - timestamp - .map { $0 as Date? } - .assign(to: \.timestamp, on: viewModel) - .store(in: &disposeBag) + viewModel.timestamp = timestamp } private func configureApplicationName(_ applicationName: String?) { @@ -294,7 +254,7 @@ extension StatusView { configure(status: originalStatus) } - func configureTranslated(status: Status) { + func configureTranslated(status: MastodonStatus) { guard let translation = viewModel.translation, let translatedContent = translation.content else { viewModel.isCurrentlyTranslating = false @@ -303,7 +263,7 @@ extension StatusView { // content do { - let content = MastodonContent(content: translatedContent, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: translatedContent, emojis: status.entity.emojis?.asDictionary ?? [:]) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent viewModel.isCurrentlyTranslating = false @@ -313,13 +273,13 @@ extension StatusView { } } - private func configureContent(statusEdit: Mastodon.Entity.StatusEdit, status: Status) { + private func configureContent(statusEdit: Mastodon.Entity.StatusEdit, status: MastodonStatus) { statusEdit.spoilerText.map { viewModel.spoilerContent = PlaintextMetaContent(string: $0) } // language - viewModel.language = (status.reblog ?? status).language + viewModel.language = (status.reblog ?? status).entity.language // content do { let content = MastodonContent(content: statusEdit.content, emojis: statusEdit.emojis?.asDictionary ?? [:]) @@ -332,7 +292,7 @@ extension StatusView { } } - private func configureContent(status: Status) { + private func configureContent(status: MastodonStatus) { guard viewModel.translation == nil else { return configureTranslated(status: status) } @@ -340,9 +300,9 @@ extension StatusView { let status = status.reblog ?? status // spoilerText - if let spoilerText = status.spoilerText, !spoilerText.isEmpty { + if let spoilerText = status.entity.spoilerText, !spoilerText.isEmpty { do { - let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: spoilerText, emojis: status.entity.emojis?.asDictionary ?? [:]) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.spoilerContent = metaContent } catch { @@ -353,10 +313,10 @@ extension StatusView { viewModel.spoilerContent = nil } // language - viewModel.language = (status.reblog ?? status).language + viewModel.language = (status.reblog ?? status).entity.language // content do { - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: status.entity.content ?? "", emojis: status.entity.emojis?.asDictionary ?? [:]) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent viewModel.isCurrentlyTranslating = false @@ -365,21 +325,18 @@ extension StatusView { viewModel.content = PlaintextMetaContent(string: "") } // visibility - status.publisher(for: \.visibilityRaw) - .compactMap { MastodonVisibility(rawValue: $0) } - .assign(to: \.visibility, on: viewModel) - .store(in: &disposeBag) + viewModel.visibility = status.entity.mastodonVisibility + // sensitive - viewModel.isContentSensitive = status.isContentSensitive - status.publisher(for: \.isSensitiveToggled) - .assign(to: \.isSensitiveToggled, on: viewModel) - .store(in: &disposeBag) + viewModel.isContentSensitive = status.entity.sensitive == true + viewModel.isSensitiveToggled = status.isSensitiveToggled + } - private func configureMedia(status: StatusCompatible) { + private func configureMedia(status: MastodonStatus) { let status = status.reblog ?? status - viewModel.isMediaSensitive = status.isMediaSensitive + viewModel.isMediaSensitive = status.entity.sensitive == true let configurations = MediaView.configuration(status: status) viewModel.mediaViewConfigurations = configurations @@ -405,39 +362,32 @@ extension StatusView { pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot) } - private func configurePoll(status: Status) { - let status = status.reblog ?? status - - if let poll = status.poll { - viewModel.objects.insert(poll) - } + private func configurePoll(status: MastodonStatus) { + Task { + guard + let context = viewModel.context?.managedObjectContext, + let poll = await status.getPoll(in: context) + else { return } + + let status = status.reblog ?? status + + viewModel.managedObjects.insert(poll) - // pollItems - status.publisher(for: \.poll) - .sink { [weak self] poll in - guard let self = self else { return } - guard let poll = poll else { - self.viewModel.pollItems = [] - return + // pollItems + let options = poll.options.sorted(by: { $0.index < $1.index }) + let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) } + self.viewModel.pollItems = items + + // isVoteButtonEnabled + poll.publisher(for: \.updatedAt) + .sink { [weak self] _ in + guard let self = self else { return } + let options = poll.options + let hasSelectedOption = options.contains(where: { $0.isSelected }) + self.viewModel.isVoteButtonEnabled = hasSelectedOption } - - let options = poll.options.sorted(by: { $0.index < $1.index }) - let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) } - self.viewModel.pollItems = items - } - .store(in: &disposeBag) - // isVoteButtonEnabled - status.poll?.publisher(for: \.updatedAt) - .sink { [weak self] _ in - guard let self = self else { return } - guard let poll = status.poll else { return } - let options = poll.options - let hasSelectedOption = options.contains(where: { $0.isSelected }) - self.viewModel.isVoteButtonEnabled = hasSelectedOption - } - .store(in: &disposeBag) - // isVotable - if let poll = status.poll { + .store(in: &disposeBag) + // isVotable Publishers.CombineLatest( poll.publisher(for: \.votedBy), poll.publisher(for: \.expired) @@ -451,100 +401,62 @@ extension StatusView { return !isVoted && !expired } .assign(to: &viewModel.$isVotable) + + // votesCount + poll.publisher(for: \.votesCount) + .map { Int($0) } + .assign(to: \.voteCount, on: viewModel) + .store(in: &disposeBag) + // voterCount + poll.publisher(for: \.votersCount) + .map { Int($0) } + .assign(to: \.voterCount, on: viewModel) + .store(in: &disposeBag) + // expireAt + poll.publisher(for: \.expiresAt) + .assign(to: \.expireAt, on: viewModel) + .store(in: &disposeBag) + // expired + poll.publisher(for: \.expired) + .assign(to: \.expired, on: viewModel) + .store(in: &disposeBag) + // isVoting + poll.publisher(for: \.isVoting) + .assign(to: \.isVoting, on: viewModel) + .store(in: &disposeBag) } - // votesCount - status.poll?.publisher(for: \.votesCount) - .map { Int($0) } - .assign(to: \.voteCount, on: viewModel) - .store(in: &disposeBag) - // voterCount - status.poll?.publisher(for: \.votersCount) - .map { Int($0) } - .assign(to: \.voterCount, on: viewModel) - .store(in: &disposeBag) - // expireAt - status.poll?.publisher(for: \.expiresAt) - .assign(to: \.expireAt, on: viewModel) - .store(in: &disposeBag) - // expired - status.poll?.publisher(for: \.expired) - .assign(to: \.expired, on: viewModel) - .store(in: &disposeBag) - // isVoting - status.poll?.publisher(for: \.isVoting) - .assign(to: \.isVoting, on: viewModel) - .store(in: &disposeBag) } - private func configureCard(status: Status) { + private func configureCard(status: MastodonStatus) { let status = status.reblog ?? status if viewModel.mediaViewConfigurations.isEmpty { - status.publisher(for: \.card) - .assign(to: \.card, on: viewModel) - .store(in: &disposeBag) + viewModel.card = status.entity.card } else { viewModel.card = nil } } - private func configureToolbar(status: Status) { + private func configureToolbar(status: MastodonStatus) { let status = status.reblog ?? status - status.publisher(for: \.repliesCount) - .map(Int.init) - .assign(to: \.replyCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.reblogsCount) - .map(Int.init) - .assign(to: \.reblogCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.favouritesCount) - .map(Int.init) - .assign(to: \.favoriteCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.editedAt) - .assign(to: \.editedAt, on: viewModel) - .store(in: &disposeBag) + viewModel.replyCount = status.entity.repliesCount ?? 0 + + viewModel.reblogCount = status.entity.reblogsCount + + viewModel.favoriteCount = status.entity.favouritesCount + + viewModel.editedAt = status.entity.editedAt // relationship - status.publisher(for: \.rebloggedBy) - .map { [weak viewModel] rebloggedBy in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return rebloggedBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain - }) - } - .assign(to: \.isReblog, on: viewModel) - .store(in: &disposeBag) - - status.publisher(for: \.favouritedBy) - .map { [weak viewModel]favouritedBy in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return favouritedBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain - }) - } - .assign(to: \.isFavorite, on: viewModel) - .store(in: &disposeBag) - - status.publisher(for: \.bookmarkedBy) - .map { [weak viewModel] bookmarkedBy in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return bookmarkedBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain - }) - } - .assign(to: \.isBookmark, on: viewModel) - .store(in: &disposeBag) + viewModel.isReblog = status.entity.reblogged == true + viewModel.isFavorite = status.entity.favourited == true + viewModel.isBookmark = status.entity.bookmarked == true } - private func configureFilter(status: Status) { + private func configureFilter(status: MastodonStatus) { let status = status.reblog ?? status - let content = status.content.lowercased() + guard let content = status.entity.content?.lowercased() else { return } Publishers.CombineLatest( viewModel.$activeFilters, @@ -595,3 +507,16 @@ extension StatusView { } } + +extension MastodonStatus { + func getPoll(in context: NSManagedObjectContext) async -> Poll? { + guard + let domain = entity.account.domain, + 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) + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 2ff5b6f85..735e4c472 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -22,11 +22,12 @@ extension StatusView { public final class ViewModel: ObservableObject { var disposeBag = Set() var observations = Set() - public var objects = Set() + public var objects = Set() + public var managedObjects = Set() public var context: AppContext? public var authContext: AuthContext? - public var originalStatus: Status? + public var originalStatus: MastodonStatus? // Header @Published public var header: Header = .none @@ -77,7 +78,7 @@ extension StatusView { @Published public var expired: Bool = false // Card - @Published public var card: Card? + @Published public var card: Mastodon.Entity.Card? // Visibility @Published public var visibility: MastodonVisibility = .public diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index c636620e6..5c57db96b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -404,7 +404,7 @@ extension StatusView { } @objc private func statusCardControlPressed(_ sender: StatusCardControl) { - guard let url = viewModel.card?.url else { return } + guard let urlString = viewModel.card?.url, let url = URL(string: urlString) else { return } delegate?.statusView(self, didTapCardWithURL: url) } diff --git a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift index 19f2bbdaa..fc8a4f365 100644 --- a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift @@ -7,7 +7,7 @@ import UIKit import Combine -import CoreDataStack +import MastodonSDK extension TimelineMiddleLoaderTableViewCell { public class ViewModel { @@ -34,15 +34,10 @@ extension TimelineMiddleLoaderTableViewCell.ViewModel { extension TimelineMiddleLoaderTableViewCell { public func configure( - feed: Feed, + feed: MastodonFeed, delegate: TimelineMiddleLoaderTableViewCellDelegate? ) { - feed.publisher(for: \.isLoadingMore) - .sink { [weak self] isLoadingMore in - guard let self = self else { return } - self.viewModel.isFetching = isLoadingMore - } - .store(in: &disposeBag) + self.viewModel.isFetching = feed.isLoadingMore self.delegate = delegate } From 9ce078ce3e048a97a54ab991d9803aa26c3382fe Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 22 Nov 2023 13:18:41 +0100 Subject: [PATCH 002/159] Fix loading of home timeline (IOS-176) --- ...ineViewController+DataSourceProvider.swift | 1 - .../HomeTimeline/HomeTimelineViewModel.swift | 28 +++---------- .../NotificationTimelineViewModel.swift | 2 +- .../FeedFetchedResultsController.swift | 40 ++++++++++++++++++- .../StatusFetchedResultsController.swift | 3 +- .../Sources/MastodonSDK/MastodonFeed.swift | 12 +++++- 6 files changed, 58 insertions(+), 28 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift index 73e72170c..a92f99961 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift @@ -21,7 +21,6 @@ extension HomeTimelineViewController: DataSourceProvider { switch item { case .feed(let feed): - let managedObjectContext = context.managedObjectContext let item: DataSourceItem? = { guard feed.kind == .home else { return nil } if let status = feed.status { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 196b72152..24d3dded7 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -80,7 +80,7 @@ final class HomeTimelineViewModel: NSObject { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.fetchedResultsController = FeedFetchedResultsController() + self.fetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext) self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) super.init() homeTimelineNeedRefresh @@ -97,6 +97,8 @@ final class HomeTimelineViewModel: NSObject { self.homeTimelineNeedRefresh.send() } .store(in: &disposeBag) + + self.fetchedResultsController.loadInitial(kind: .home) } } @@ -110,7 +112,7 @@ extension HomeTimelineViewModel { extension HomeTimelineViewModel { func timelineDidReachEnd() { - #warning("Check if required, e.g. when locally caching MastodonStatus") + fetchedResultsController.loadNext(kind: .home) } } @@ -122,29 +124,9 @@ extension HomeTimelineViewModel { guard let diffableDataSource = diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() - -// let managedObjectContext = context.managedObjectContext -// let key = "LoadMore@\(record.objectID)" -// -// guard let feed = record.object(in: managedObjectContext) else { return } - guard let status = record.status else { return } record.isLoadingMore = true - -// // keep transient property live -// managedObjectContext.cache(feed, key: key) -// defer { -// managedObjectContext.cache(nil, key: key) -// } -// do { -// // update state -// try await managedObjectContext.performChanges { -// feed.update(isLoadingMore: true) -// } -// } catch { -// assertionFailure(error.localizedDescription) -// } - + // reconfigure item snapshot.reconfigureItems([item]) await updateSnapshotUsingReloadData(snapshot: snapshot) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index f4c082909..b3f728b5d 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -52,7 +52,7 @@ final class NotificationTimelineViewModel { self.context = context self.authContext = authContext self.scope = scope - self.feedFetchedResultsController = FeedFetchedResultsController() + self.feedFetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext) } diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index c578ce404..ada088aa1 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -14,6 +14,44 @@ import MastodonSDK final public class FeedFetchedResultsController { @Published public var records: [MastodonFeed] = [] + + private let context: AppContext + private let authContext: AuthContext - public init() {} + 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) + } + } +} + +private extension FeedFetchedResultsController { + 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) } + } + } } diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index d34cd1add..22e1a49c4 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -13,7 +13,8 @@ import MastodonSDK public final class StatusFetchedResultsController { @MainActor - @Published public private(set) var records: [MastodonStatus] = [] + @Published + public private(set) var records: [MastodonStatus] = [] @MainActor public init(records: [MastodonStatus] = []) { diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index 463909e72..1ffba4e82 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -4,6 +4,14 @@ import Foundation import CoreDataStack public final class MastodonFeed { + + public enum Kind { + case home + case notificationAll + case notificationMentions + } + + public let id: String public var hasMore: Bool = false public var isLoadingMore: Bool = false @@ -13,6 +21,7 @@ public final class MastodonFeed { public let kind: Feed.Kind init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, kind: Feed.Kind) { + self.id = status?.id ?? notification?.id ?? UUID().uuidString self.hasMore = hasMore self.isLoadingMore = isLoadingMore self.status = status @@ -45,10 +54,11 @@ public extension MastodonFeed { extension MastodonFeed: Hashable { public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool { - lhs.status?.id == rhs.status?.id || lhs.notification?.id == rhs.notification?.id + lhs.id == rhs.id && (lhs.status?.id == rhs.status?.id || lhs.notification?.id == rhs.notification?.id) } public func hash(into hasher: inout Hasher) { + hasher.combine(id) hasher.combine(status) hasher.combine(notification) } From d0893d5c6958b778da97063f0843e92a6fec0382 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 22 Nov 2023 13:18:51 +0100 Subject: [PATCH 003/159] Fix notifications timeline loading (IOS-176) --- .../NotificationTimelineViewModel.swift | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index b3f728b5d..ca739ed55 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -106,31 +106,35 @@ extension NotificationTimelineViewModel { isLoadingLatest = true defer { isLoadingLatest = false } - do { - _ = try await context.apiService.notifications( - maxID: nil, - scope: scope, - authenticationBox: authContext.mastodonAuthenticationBox - ) - } catch { - didLoadLatest.send() + switch scope { + case .everything: + feedFetchedResultsController.loadInitial(kind: .notificationAll) + case .mentions: + feedFetchedResultsController.loadInitial(kind: .notificationMentions) } + + didLoadLatest.send() } // load timeline gap func loadMore(item: NotificationItem) async { - guard case let .feedLoader(record) = item else { return } +// guard case let .feedLoader(record) = item else { return } - guard let maxID = record.notification?.id else { return } +// guard let maxID = record.notification?.id else { return } - // fetch data - if let notifications = try? await context.apiService.notifications( - maxID: maxID, - scope: scope, - authenticationBox: authContext.mastodonAuthenticationBox - ) { - self.feedFetchedResultsController.records += notifications.value.map { MastodonFeed.fromNotification($0, kind: record.kind) } +// // fetch data +// if let notifications = try? await context.apiService.notifications( +// maxID: maxID, +// scope: scope, +// authenticationBox: authContext.mastodonAuthenticationBox +// ) { +// self.feedFetchedResultsController.records += notifications.value.map { MastodonFeed.fromNotification($0, kind: record.kind) } +// } + switch scope { + case .everything: + feedFetchedResultsController.loadNext(kind: .notificationAll) + case .mentions: + feedFetchedResultsController.loadNext(kind: .notificationMentions) } } - } From 36d4f6ea1533ac1bb53a8402ef57fc776ebbeeaa Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 22 Nov 2023 17:01:59 +0100 Subject: [PATCH 004/159] Fix threads not working (IOS-176) --- .../MastodonStatusThreadViewModel.swift | 125 ++++++------------ .../ThreadViewModel+LoadThreadState.swift | 2 +- 2 files changed, 41 insertions(+), 86 deletions(-) diff --git a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift index 8fb717cd1..08e1c7c0f 100644 --- a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift +++ b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift @@ -80,34 +80,11 @@ extension MastodonStatusThreadViewModel { domain: String, nodes: [Node] ) { - let ids = nodes.map { $0.statusID } - var dictionary: [Status.ID: Status] = [:] - do { - let request = Status.sortedFetchRequest - request.predicate = Status.predicate(domain: domain, ids: ids) - let statuses = try self.context.managedObjectContext.fetch(request) - for status in statuses { - dictionary[status.id] = status - } - } catch { - return - } - var newItems: [StatusItem] = [] - #warning("Potentially this can be removed and replaced by native threading logic") -// for (i, node) in nodes.enumerated() { -// guard let status = dictionary[node.statusID] else { continue } -// let isLast = i == nodes.count - 1 -// -// let record = ManagedObjectRecord(objectID: status.objectID) -// let context = StatusItem.Thread.Context( -// status: record, -// displayUpperConversationLink: !isLast, -// displayBottomConversationLink: true -// ) -// let item = StatusItem.thread(.leaf(context: context)) -// newItems.append(item) -// } + for node in nodes { + let item = StatusItem.thread(.leaf(context: .init(status: node.status))) + newItems.append(item) + } let items = self.__ancestors + newItems self.__ancestors = items @@ -117,48 +94,28 @@ extension MastodonStatusThreadViewModel { domain: String, nodes: [Node] ) { - let childrenIDs = nodes - .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } - .flatMap { $0 } - var dictionary: [Status.ID: Status] = [:] - do { - let request = Status.sortedFetchRequest - request.predicate = Status.predicate(domain: domain, ids: childrenIDs) - let statuses = try self.context.managedObjectContext.fetch(request) - for status in statuses { - dictionary[status.id] = status - } - } catch { - return - } - + var newItems: [StatusItem] = [] -#warning("Potentially this can be removed and replaced by native threading logic") -// for node in nodes { -// guard let status = dictionary[node.statusID] else { continue } -// // first tier -// let record = ManagedObjectRecord(objectID: status.objectID) -// let context = StatusItem.Thread.Context( -// status: record -// ) -// let item = StatusItem.thread(.leaf(context: context)) -// newItems.append(item) -// -// // second tier -// if let child = node.children.first { -// guard let secondaryStatus = dictionary[child.statusID] else { continue } -// let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) -// let secondaryContext = StatusItem.Thread.Context( -// status: secondaryRecord, -// displayUpperConversationLink: true -// ) -// let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) -// newItems.append(secondaryItem) -// -// // update first tier context -// context.displayBottomConversationLink = true -// } -// } + + for node in nodes { + let context = StatusItem.Thread.Context(status: node.status) + let item = StatusItem.thread(.leaf(context: context)) + newItems.append(item) + + // second tier + if let child = node.children.first { + guard let secondaryStatus = node.children.first(where: { $0.status.id == child.status.id}) else { continue } + let secondaryContext = StatusItem.Thread.Context( + status: secondaryStatus.status, + displayUpperConversationLink: true + ) + let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) + newItems.append(secondaryItem) + + // update first tier context + context.displayBottomConversationLink = true + } + } var items = self.__descendants for item in newItems { @@ -172,16 +129,14 @@ extension MastodonStatusThreadViewModel { extension MastodonStatusThreadViewModel { class Node { - typealias ID = String - - let statusID: ID + let status: MastodonStatus let children: [Node] init( - statusID: ID, + status: MastodonStatus, children: [MastodonStatusThreadViewModel.Node] ) { - self.statusID = statusID + self.status = status self.children = children } } @@ -206,7 +161,7 @@ extension MastodonStatusThreadViewModel.Node { while let _nextID = nextID { guard let status = dict[_nextID] else { break } nodes.append(MastodonStatusThreadViewModel.Node( - statusID: _nextID, + status: .fromEntity(status), children: [] )) nextID = status.inReplyToID @@ -218,11 +173,11 @@ extension MastodonStatusThreadViewModel.Node { extension MastodonStatusThreadViewModel.Node { static func children( - of statusID: ID, + of status: MastodonStatus, from statuses: [Mastodon.Entity.Status] ) -> [MastodonStatusThreadViewModel.Node] { - var dictionary: [ID: Mastodon.Entity.Status] = [:] - var mapping: [ID: Set] = [:] + var dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:] + var mapping: [Mastodon.Entity.Status.ID: Set] = [:] for status in statuses { dictionary[status.id] = status @@ -236,28 +191,28 @@ extension MastodonStatusThreadViewModel.Node { } var children: [MastodonStatusThreadViewModel.Node] = [] - let replies = Array(mapping[statusID] ?? Set()) + let replies = Array(mapping[status.id] ?? Set()) .compactMap { dictionary[$0] } .sorted(by: { $0.createdAt > $1.createdAt }) for reply in replies { - let child = child(of: reply.id, dictionary: dictionary, mapping: mapping) + let child = child(of: reply, dictionary: dictionary, mapping: mapping) children.append(child) } return children } static func child( - of statusID: ID, - dictionary: [ID: Mastodon.Entity.Status], - mapping: [ID: Set] + of status: Mastodon.Entity.Status, + dictionary: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status], + mapping: [Mastodon.Entity.Status.ID: Set] ) -> MastodonStatusThreadViewModel.Node { - let childrenIDs = mapping[statusID] ?? [] + let childrenIDs = mapping[status.id] ?? [] let children = Array(childrenIDs) .compactMap { dictionary[$0] } .sorted(by: { $0.createdAt > $1.createdAt }) - .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) } + .map { status in child(of: status, dictionary: dictionary, mapping: mapping) } return MastodonStatusThreadViewModel.Node( - statusID: statusID, + status: .fromEntity(status), children: children ) } diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 4679e0976..dff7dc047 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -84,7 +84,7 @@ extension ThreadViewModel.LoadThreadState { viewModel.mastodonStatusThreadViewModel.appendDescendant( domain: threadContext.domain, nodes: response.value.descendants.map { status in - return .init(statusID: status.id, children: []) + return .init(status: .fromEntity(status), children: []) } ) } catch { From 9809e697515522f65de8ff9c79ef0dcab83bbfdf Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 22 Nov 2023 21:16:48 +0100 Subject: [PATCH 005/159] Fix favorite action (IOS-176) --- Mastodon/Protocol/Provider/DataSourceFacade+Status.swift | 9 ++++----- .../MastodonCore/Service/API/APIService+Favorite.swift | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index fdf39b876..5557c61e4 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -99,7 +99,6 @@ extension DataSourceFacade { action: ActionToolbarContainer.Action, sender: UIButton ) async throws { - let managedObjectContext = provider.context.managedObjectContext let _status = status.reblog ?? status switch action { @@ -111,7 +110,7 @@ extension DataSourceFacade { context: provider.context, authContext: provider.authContext, composeContext: .composeStatus, - destination: .reply(parent: status) + destination: .reply(parent: _status) ) _ = provider.coordinator.present( scene: .compose(viewModel: composeViewModel), @@ -121,17 +120,17 @@ extension DataSourceFacade { case .reblog: try await DataSourceFacade.responseToStatusReblogAction( provider: provider, - status: status + status: _status ) case .like: try await DataSourceFacade.responseToStatusFavoriteAction( provider: provider, - status: status + status: _status ) case .share: try await DataSourceFacade.responseToStatusShareAction( provider: provider, - status: status, + status: _status, button: sender ) } // end switch diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index e5bd28231..a1e8371b6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -37,11 +37,11 @@ extension APIService { } let _status = status.reblog ?? status - let isFavorited = status.entity.favourited == true - let favoritedCount = Int64(status.entity.favouritesCount) + let isFavorited = _status.entity.favourited == true + let favoritedCount = Int64(_status.entity.favouritesCount) let context = MastodonFavoriteContext( - statusID: status.id, + statusID: _status.id, isFavorited: isFavorited, favoritedCount: favoritedCount ) From e0671eb324de89fe2705d071aef1dd64ae161753 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 23 Nov 2023 11:21:52 +0100 Subject: [PATCH 006/159] Reimplement favorite/reblog state (IOS-176) --- .../Provider/DataSourceFacade+Favorite.swift | 6 +++-- .../Provider/DataSourceFacade+Reblog.swift | 8 ++++--- ...Provider+StatusTableViewCellDelegate.swift | 2 +- .../Provider/DataSourceProvider.swift | 1 + ...ityViewController+DataSourceProvider.swift | 5 +++++ ...stsViewController+DataSourceProvider.swift | 5 +++++ ...ineViewController+DataSourceProvider.swift | 5 +++++ ...ineViewController+DataSourceProvider.swift | 5 +++++ ...ineViewController+DataSourceProvider.swift | 4 ++++ ...arkViewController+DataSourceProvider.swift | 5 +++++ ...ersViewController+DataSourceProvider.swift | 5 +++++ ...iteViewController+DataSourceProvider.swift | 5 +++++ .../Follower/FollowerListViewController.swift | 5 +++++ .../FollowingListViewController.swift | 5 +++++ ...ineViewController+DataSourceProvider.swift | 5 +++++ ...dByViewController+DataSourceProvider.swift | 5 +++++ ...dByViewController+DataSourceProvider.swift | 6 +++++ ...oryViewController+DataSourceProvider.swift | 5 +++++ ...ultViewController+DataSourceProvider.swift | 5 +++++ ...eadViewController+DataSourceProvider.swift | 5 +++++ .../FeedFetchedResultsController.swift | 22 +++++++++++++++++++ .../StatusFetchedResultsController.swift | 13 +++++++++++ .../Service/API/APIService+Reblog.swift | 2 +- .../Entity/Mastodon+Entity+Status.swift | 13 ++++++++++- .../Sources/MastodonSDK/MastodonFeed.swift | 8 ++++--- .../Sources/MastodonSDK/MastodonStatus.swift | 7 +++--- 26 files changed, 148 insertions(+), 14 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift index 8797ea986..e2a4397e5 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -18,9 +18,11 @@ extension DataSourceFacade { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - _ = try await provider.context.apiService.favorite( + let newStatus = try await provider.context.apiService.favorite( status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox - ) + ).value + + provider.update(status: .fromEntity(newStatus)) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index c16a9e415..1d445697e 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -18,9 +18,11 @@ extension DataSourceFacade { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - _ = try await provider.context.apiService.reblog( + let newStatus = try await provider.context.apiService.reblog( status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox - ) - } // end func + ).value + + provider.update(status: .fromEntity(newStatus)) + } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 554bb92f1..86f301488 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -439,7 +439,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte assertionFailure("only works for status data provider") return } - + try await DataSourceFacade.responseToActionToolbar( provider: self, status: status, diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index ab6df8c2c..053006ef8 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -46,4 +46,5 @@ extension DataSourceItem { protocol DataSourceProvider: ViewControllerWithDependencies { func item(from source: DataSourceItem.Source) async -> DataSourceItem? + func update(status: MastodonStatus) } diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift index e9441b5d3..44db77f5b 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension DiscoveryCommunityViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension DiscoveryCommunityViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.statusFetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift index c3495b245..300235ded 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension DiscoveryPostsViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension DiscoveryPostsViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.statusFetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift index 6cd97fcca..21bf2c79e 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension HashtagTimelineViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension HashtagTimelineViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.fetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift index a92f99961..9636ac6b4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension HomeTimelineViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -35,6 +36,10 @@ extension HomeTimelineViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.fetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index 5ccc2a2ff..a15f1664c 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -37,6 +37,10 @@ extension NotificationTimelineViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.feedFetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift index a22fb4309..7b0252f62 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension BookmarkViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension BookmarkViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.statusFetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController+DataSourceProvider.swift index c014a7900..ce250090e 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension FamiliarFollowersViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension FamiliarFollowersViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + assertionFailure("Implement not required in this class") + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift index 8fe8d1bd7..7fa1ced50 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension FavoriteViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension FavoriteViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.statusFetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index daa2838a5..48c66f36a 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -11,6 +11,7 @@ import Combine import MastodonCore import MastodonUI import MastodonLocalization +import MastodonSDK final class FollowerListViewController: UIViewController, NeedsDependency { @@ -152,6 +153,10 @@ extension FollowerListViewController: DataSourceProvider { return nil } } + + func update(status: MastodonStatus) { + assertionFailure("Implement not required in this class") + } @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index 28fdac266..5bb36a9fe 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -12,6 +12,7 @@ import MastodonLocalization import MastodonCore import MastodonUI import CoreDataStack +import MastodonSDK final class FollowingListViewController: UIViewController, NeedsDependency { @@ -148,6 +149,10 @@ extension FollowingListViewController: DataSourceProvider { return nil } } + + func update(status: MastodonStatus) { + assertionFailure("Implement not required in this class") + } @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift index 2b18fad56..3c536888d 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension UserTimelineViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension UserTimelineViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.statusFetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift index 437873d36..4fbde2165 100644 --- a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK extension FavoritedByViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { @@ -27,6 +28,10 @@ extension FavoritedByViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + assertionFailure("Implement not required in this class") + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift index 04d5d2596..c111d5491 100644 --- a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift @@ -6,8 +6,10 @@ // import UIKit +import MastodonSDK extension RebloggedByViewController: DataSourceProvider { + func item(from source: DataSourceItem.Source) async -> DataSourceItem? { var _indexPath = source.indexPath if _indexPath == nil, let cell = source.tableViewCell { @@ -27,6 +29,10 @@ extension RebloggedByViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + assertionFailure("Implement not required in this class") + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift index a1bae2638..e9b57c91d 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK // MARK: - DataSourceProvider extension SearchHistoryViewController: DataSourceProvider { @@ -28,6 +29,10 @@ extension SearchHistoryViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + assertionFailure("Implement not required in this class") + } + @MainActor private func indexPath(for cell: UICollectionViewCell) async -> IndexPath? { return collectionView.indexPath(for: cell) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index 5e9d9e2db..895d57237 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK // MARK: - DataSourceProvider extension SearchResultViewController: DataSourceProvider { @@ -32,6 +33,10 @@ extension SearchResultViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.statusFetchedResultsController.update(status: status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift index 06d510a82..3ea91d0d5 100644 --- a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift @@ -6,6 +6,7 @@ // import UIKit +import MastodonSDK // MARK: - DataSourceProvider extension ThreadViewController: DataSourceProvider { @@ -28,6 +29,10 @@ extension ThreadViewController: DataSourceProvider { } } + func update(status: MastodonStatus) { + viewModel.root = .root(context: .init(status: status)) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index ada088aa1..3a9eecae0 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -38,6 +38,28 @@ final public class FeedFetchedResultsController { 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 { + newRecords[i] = .fromStatus(status, kind: record.kind) + switch status.entity.reblogged { + case .some(true): + newRecords[i].status?.reblog = status + case .some(false): + newRecords[i].status?.reblog = nil + case .none: + break + } + } + } + records = newRecords + } } private extension FeedFetchedResultsController { diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 22e1a49c4..67572a766 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -35,4 +35,17 @@ public final class StatusFetchedResultsController { public func appendRecords(_ records: [MastodonStatus]) { self.records += records } + + @MainActor + public func update(status: MastodonStatus) { + var newRecords = Array(records) + for (i, record) in newRecords.enumerated() { + if record.id == status.id { + newRecords[i] = status + } else if let reblog = record.reblog, reblog.id == status.id { + newRecords[i].reblog = status + } + } + records = newRecords + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index 6bcc8dab1..eea8b7fbf 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -39,7 +39,7 @@ extension APIService { let reblogContext = MastodonReblogContext( statusID: _status.id, - isReblogged: !isReblogged, + isReblogged: isReblogged, rebloggedCount: rebloggedCount ) return reblogContext diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index b56b9067d..bcfbf33f5 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -136,11 +136,22 @@ extension Mastodon.Entity.Status { extension Mastodon.Entity.Status: Hashable { public static func == (lhs: Mastodon.Entity.Status, rhs: Mastodon.Entity.Status) -> Bool { - lhs.uri == rhs.uri && lhs.id == rhs.id + lhs.uri == rhs.uri && + lhs.id == rhs.id && + lhs.reblog == rhs.reblog && + lhs.favourited == rhs.favourited && + lhs.reblogged == rhs.reblogged && + lhs.bookmarked == rhs.bookmarked && + lhs.pinned == rhs.pinned } public func hash(into hasher: inout Hasher) { hasher.combine(uri) hasher.combine(id) + hasher.combine(reblog) + hasher.combine(favourited) + hasher.combine(reblogged) + hasher.combine(bookmarked) + hasher.combine(pinned) } } diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index 1ffba4e82..4b580c01d 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -54,13 +54,15 @@ public extension MastodonFeed { extension MastodonFeed: Hashable { public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool { - lhs.id == rhs.id && (lhs.status?.id == rhs.status?.id || lhs.notification?.id == rhs.notification?.id) + lhs.id == rhs.id && + lhs.status?.entity == rhs.status?.entity && + lhs.status?.reblog?.entity == rhs.status?.reblog?.entity } public func hash(into hasher: inout Hasher) { hasher.combine(id) - hasher.combine(status) - hasher.combine(notification) + hasher.combine(status?.entity) + hasher.combine(status?.reblog?.entity) } } diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift index 24dbfa3a7..71bc07b2c 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift @@ -8,7 +8,7 @@ public final class MastodonStatus: ObservableObject { public typealias ID = Mastodon.Entity.Status.ID @Published public var entity: Mastodon.Entity.Status - @Published public private(set) var reblog: MastodonStatus? + @Published public var reblog: MastodonStatus? @Published public var isSensitiveToggled: Bool = false @@ -36,12 +36,13 @@ extension MastodonStatus { extension MastodonStatus: Hashable { public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool { - lhs.entity.id == rhs.entity.id + lhs.entity == rhs.entity && + lhs.reblog?.entity == rhs.reblog?.entity } public func hash(into hasher: inout Hasher) { hasher.combine(entity) - hasher.combine(isSensitiveToggled) + hasher.combine(reblog?.entity) } } From c7d62d56a2bcf08b52ff511a36c17b8a123c06a7 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 27 Nov 2023 11:18:43 +0100 Subject: [PATCH 007/159] Fix handling of (re-)reblogged and favorites state in home timeline (IOS-176) --- .../FeedFetchedResultsController.swift | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 3a9eecae0..8b630b274 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -47,15 +47,24 @@ final public class FeedFetchedResultsController { } 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 { - newRecords[i] = .fromStatus(status, kind: record.kind) + // Handle reblogged state switch status.entity.reblogged { case .some(true): - newRecords[i].status?.reblog = status - case .some(false): - newRecords[i].status?.reblog = nil - case .none: - break + newRecords[i] = .fromStatus({ + let stat = MastodonStatus.fromEntity(records[i].status!.entity) + stat.reblog = .fromEntity(status.entity) + return stat + }(), kind: record.kind) + case .some(false), .none: + newRecords[i] = .fromStatus({ + let stat = MastodonStatus.fromEntity(status.entity) + 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 From 4cb845e0bd442dbd48e9568437465bf009b77d2a Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 27 Nov 2023 11:35:57 +0100 Subject: [PATCH 008/159] Fix manual refresh of home timeline not working (IOS-176) --- .../HomeTimelineViewModel+LoadLatestState.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index cfad3f24c..b107f8efc 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -11,6 +11,7 @@ import CoreData import CoreDataStack import GameplayKit import MastodonCore +import MastodonSDK extension HomeTimelineViewModel { class LoadLatestState: GKState { @@ -110,6 +111,11 @@ extension HomeTimelineViewModel.LoadLatestState { if !latestStatusIDs.isEmpty { viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming() } + + let newRecords: [MastodonFeed] = newStatuses.map { + MastodonFeed.fromStatus(.fromEntity($0), kind: .home) + } + viewModel.fetchedResultsController.records = newRecords + viewModel.fetchedResultsController.records } viewModel.timelineIsEmpty.value = latestStatusIDs.isEmpty && statuses.isEmpty From faac99cd102beca375de778bcf4da45e1a295ec2 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 27 Nov 2023 11:54:01 +0100 Subject: [PATCH 009/159] Fix duplicate entry after reblog (IOS-176) --- .../HomeTimelineViewModel+LoadLatestState.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index b107f8efc..19a58c80b 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -112,10 +112,21 @@ extension HomeTimelineViewModel.LoadLatestState { viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming() } - let newRecords: [MastodonFeed] = newStatuses.map { + var newRecords: [MastodonFeed] = newStatuses.map { MastodonFeed.fromStatus(.fromEntity($0), kind: .home) } - viewModel.fetchedResultsController.records = newRecords + viewModel.fetchedResultsController.records + viewModel.fetchedResultsController.records = { + var oldRecords = viewModel.fetchedResultsController.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 + if newRecords.count > index { + newRecords.remove(at: i) + } + } + } + return (newRecords + oldRecords).removingDuplicates() + }() } viewModel.timelineIsEmpty.value = latestStatusIDs.isEmpty && statuses.isEmpty From 9c5e1936737f5bcb8816b451178cac849455a588 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 27 Nov 2023 13:44:26 +0100 Subject: [PATCH 010/159] Reimplement timeline anchoring (IOS-176) --- .../HomeTimelineViewModel+Diffable.swift | 47 ++++++------------- ...tificationTimelineViewModel+Diffable.swift | 45 +++++------------- 2 files changed, 27 insertions(+), 65 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 769ea44cc..d2ea8b878 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -6,9 +6,8 @@ // import UIKit -import CoreData -import CoreDataStack import MastodonUI +import MastodonSDK extension HomeTimelineViewModel { @@ -52,37 +51,19 @@ extension HomeTimelineViewModel { snapshot.appendItems(newItems, toSection: .main) return snapshot }() - #warning("We probably need to replace the code below") -// let parentManagedObjectContext = self.context.managedObjectContext -// let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) -// managedObjectContext.parent = parentManagedObjectContext -// try? await managedObjectContext.perform { -// let anchors: [Feed] = { -// let request = Feed.sortedFetchRequest -// request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ -// Feed.hasMorePredicate(), -// self.fetchedResultsController.predicate, -// ]) -// do { -// return try managedObjectContext.fetch(request) -// } catch { -// assertionFailure(error.localizedDescription) -// return [] -// } -// }() -// -// let itemIdentifiers = newSnapshot.itemIdentifiers -// for (index, item) in itemIdentifiers.enumerated() { -// guard case let .feed(record) = item else { continue } -// guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue } -// let isLast = index + 1 == itemIdentifiers.count -// if isLast { -// newSnapshot.insertItems([.bottomLoader], afterItem: item) -// } else { -// newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) -// } -// } -// } + + let anchors: [MastodonFeed] = records.filter { $0.hasMore == true } + let itemIdentifiers = newSnapshot.itemIdentifiers + for (index, item) in itemIdentifiers.enumerated() { + guard case let .feed(record) = item else { continue } + guard anchors.contains(where: { feed in feed.id == record.id }) else { continue } + let isLast = index + 1 == itemIdentifiers.count + if isLast { + newSnapshot.insertItems([.bottomLoader], afterItem: item) + } else { + newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) + } + } let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers if !hasChanges && !self.hasPendingStatusEditReload { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index e3460a72b..bc827511c 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -7,7 +7,7 @@ import UIKit import CoreData -import CoreDataStack +import MastodonSDK extension NotificationTimelineViewModel { @@ -48,37 +48,18 @@ extension NotificationTimelineViewModel { return snapshot }() - #warning("Do we still need the code below?") -// let parentManagedObjectContext = self.context.managedObjectContext -// let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) -// managedObjectContext.parent = parentManagedObjectContext -// try? await managedObjectContext.perform { -// let anchors: [Feed] = { -// let request = Feed.sortedFetchRequest -// request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ -// Feed.hasMorePredicate(), -// self.feedFetchedResultsController.predicate, -// ]) -// do { -// return try managedObjectContext.fetch(request) -// } catch { -// assertionFailure(error.localizedDescription) -// return [] -// } -// }() -// -// let itemIdentifiers = newSnapshot.itemIdentifiers -// for (index, item) in itemIdentifiers.enumerated() { -// guard case let .feed(record) = item else { continue } -// guard anchors.contains(where: { feed in feed.objectID == record.objectID }) else { continue } -// let isLast = index + 1 == itemIdentifiers.count -// if isLast { -// newSnapshot.insertItems([.bottomLoader], afterItem: item) -// } else { -// newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) -// } -// } -// } + let anchors: [MastodonFeed] = records.filter { $0.hasMore == true } + let itemIdentifiers = newSnapshot.itemIdentifiers + for (index, item) in itemIdentifiers.enumerated() { + guard case let .feed(record) = item else { continue } + guard anchors.contains(where: { feed in feed.id == record.id }) else { continue } + let isLast = index + 1 == itemIdentifiers.count + if isLast { + newSnapshot.insertItems([.bottomLoader], afterItem: item) + } else { + newSnapshot.insertItems([.feedLoader(record: record)], afterItem: item) + } + } let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers if !hasChanges { From 831665141c8ce0b3f44fd231e2b72b4296f13a63 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 27 Nov 2023 14:19:17 +0100 Subject: [PATCH 011/159] Fix tabbar avatar not visible (IOS-176) --- .../Root/MainTab/MainTabBarController.swift | 232 +++++++++--------- 1 file changed, 117 insertions(+), 115 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 3d7852ad7..331c84264 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -190,131 +190,133 @@ extension MainTabBarController { _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 { - case .implicit: - break - case .explicit: - let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) - alertController.addAction(okAction) - _ = coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) + + + // 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 } } - .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 - } + 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: + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + _ = coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + } + } + .store(in: &disposeBag) - notificationViewController.tabBarItem.image = image.imageWithoutBaseline() - notificationViewController.navigationController?.tabBarItem.image = image.imageWithoutBaseline() - } - .store(in: &disposeBag) - layoutAvatarButton() - - $avatarURL + // 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] avatarURL in + .sink { [weak self] authentication, currentTab 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 = self else { return } - if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) { - self.avatarURLObserver = user.publisher(for: \.avatar) - .sink { [weak self, weak user] _ in - guard let self = self else { return } - guard let user = user else { return } - guard user.managedObjectContext != nil else { return } - self.avatarURL = user.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(user.displayNameWithFallback) - - self.context.authenticationService.updateActiveUserAccountPublisher - .sink { [weak self] in - self?.updateUserAccount() - } - .store(in: &self.disposeBag) + 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 { - self.avatarURLObserver = nil + image = Tab.notifications.image } + + notificationViewController.tabBarItem.image = image.imageWithoutBaseline() + notificationViewController.navigationController?.tabBarItem.image = image.imageWithoutBaseline() } .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() + 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 + ) + // self.avatarButton.avatarImageView.image = UIImage(named: "AppIcon") + } + .store(in: &disposeBag) + + NotificationCenter.default.publisher(for: .userFetched) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) { + self.avatarURLObserver = user.publisher(for: \.avatar) + .sink { [weak self, weak user] _ in + guard let self = self else { return } + guard let user = user else { return } + guard user.managedObjectContext != nil else { return } + self.avatarURL = user.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(user.displayNameWithFallback) + + self.context.authenticationService.updateActiveUserAccountPublisher + .sink { [weak self] in + self?.updateUserAccount() + } + .store(in: &self.disposeBag) + } else { + self.avatarURLObserver = nil + } + } + .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?) { From a322a46fdf759f4ee394bf1d31d792a6330ec579 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 28 Nov 2023 09:13:24 +0100 Subject: [PATCH 012/159] Don't try to resolve self-relationship --- .../Content/StatusView+Configuration.swift | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 647c31ba2..245521d3d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -184,31 +184,45 @@ extension StatusView { } public func configureAuthor(author: Mastodon.Entity.Account) { - // author avatar - viewModel.authorAvatarImageURL = author.avatarImageURL() - let emojis = author.emojis?.asDictionary ?? [:] - - // author name - viewModel.authorName = { - do { - let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure(error.localizedDescription) - return PlaintextMetaContent(string: author.displayNameWithFallback) - } - }() - - // author username - viewModel.authorUsername = author.acct - - // locked - viewModel.locked = author.locked - - // isMuting, isBlocking, Following Task { + + // author avatar + viewModel.authorAvatarImageURL = author.avatarImageURL() + let emojis = author.emojis?.asDictionary ?? [:] + + // author name + viewModel.authorName = { + do { + let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis) + let metaContent = try MastodonMetaContent.convert(document: content) + return metaContent + } catch { + assertionFailure(error.localizedDescription) + return PlaintextMetaContent(string: author.displayNameWithFallback) + } + }() + + // author username + viewModel.authorUsername = author.acct + + // locked + viewModel.locked = author.locked + + // isMyself + viewModel.isMyself = { + guard let authContext = viewModel.authContext else { return false } + return authContext.mastodonAuthenticationBox.domain == author.domain && authContext.mastodonAuthenticationBox.userID == author.id + }() + + // isMuting, isBlocking, Following guard let auth = viewModel.authContext?.mastodonAuthenticationBox else { return } + guard !viewModel.isMyself else { + viewModel.isMuting = false + viewModel.isBlocking = false + viewModel.isFollowed = false + return + } + if let relationship = try? await Mastodon.API.Account.relationships( session: .shared, domain: auth.domain, @@ -223,13 +237,6 @@ extension StatusView { } } } - - // isMyself - viewModel.isMyself = { - guard let authContext = viewModel.authContext else { return false } - return authContext.mastodonAuthenticationBox.domain == author.domain && authContext.mastodonAuthenticationBox.userID == author.id - }() - } private func configureTimestamp(timestamp: Date) { From 167e0e4616d957d0b48ad1eeccb4e90bf55964e9 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 28 Nov 2023 09:41:34 +0100 Subject: [PATCH 013/159] Fix translate not working (IOS-176) --- .../DataSourceProvider+StatusTableViewCellDelegate.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 86f301488..5550b177d 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -465,13 +465,14 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte assertionFailure() return } - guard case let .status(status) = item else { + guard case let .status(_status) = item else { assertionFailure("only works for status data provider") return } let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { + let status = _status.reblog ?? _status let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: status.entity.account.domain ?? "", id: status.entity.account.id) + request.predicate = MastodonUser.predicate(domain: self.authContext.mastodonAuthenticationBox.domain, id: status.entity.account.id) request.fetchLimit = 1 guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } return .init(objectID: author.objectID) From 9fed54db1fcfa187576b1d22a424e14c44aa294c Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 28 Nov 2023 09:55:18 +0100 Subject: [PATCH 014/159] Fix compile issues after resolving merge conflicts --- .../SearchResult/SearchResultSection.swift | 1 + .../SearchResult/SearchResultViewModel+State.swift | 13 ++----------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index 042b6f489..42b1a1585 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -54,6 +54,7 @@ extension SearchResultSection { relationship: relationship, delegate: configuration.userTableViewCellDelegate ) + return cell case .status(let status): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell configure( diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index 97624057e..832acb4e3 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -123,10 +123,8 @@ extension SearchResultViewModel.State { // discard result when state is not Loading guard stateMachine.currentState is Loading else { return } -//<<<<<<< remove_status -// let userIDs = response.value.accounts.map { $0.id } -// let statusIDs = response.value.statuses.map { MastodonStatus.fromEntity($0) } -//======= + let statusIDs = searchResults.statuses.map { MastodonStatus.fromEntity($0) } + let accounts = searchResults.accounts let relationships = try await viewModel.context.apiService.relationship( @@ -134,9 +132,6 @@ extension SearchResultViewModel.State { authenticationBox: viewModel.authContext.mastodonAuthenticationBox ).value - let statusIDs = searchResults.statuses.map { $0.id } -//>>>>>>> develop - let isNoMore = accounts.isEmpty && statusIDs.isEmpty if viewModel.searchScope == .all || isNoMore { @@ -150,11 +145,9 @@ extension SearchResultViewModel.State { await viewModel.statusFetchedResultsController.reset() viewModel.relationships = [] viewModel.accounts = [] - //viewModel.statusFetchedResultsController.statusIDs = [] viewModel.hashtags = [] } - viewModel.userFetchedResultsController.append(userIDs: userIDs) await viewModel.statusFetchedResultsController.appendRecords(statusIDs) @@ -163,8 +156,6 @@ extension SearchResultViewModel.State { existingRelationships.append(hashtag) } viewModel.relationships = existingRelationships - - viewModel.statusFetchedResultsController.append(statusIDs: statusIDs) var existingHashtags = viewModel.hashtags for hashtag in searchResults.hashtags where !existingHashtags.contains(hashtag) { From 45accb29e64fda9c11617ed52bfcf374157eec0e Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 28 Nov 2023 10:46:24 +0100 Subject: [PATCH 015/159] Fix CW cannot be hidden (IOS-176) --- .../Provider/DataSourceFacade+Status.swift | 14 ++++++++------ Mastodon/Scene/Profile/ProfileViewController.swift | 12 ++++++++++++ MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift | 4 +++- .../Sources/MastodonSDK/MastodonStatus.swift | 4 +++- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 5557c61e4..70fd66e7c 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -150,7 +150,7 @@ extension DataSourceFacade { @MainActor static func responseToMenuAction( - dependency: UIViewController & NeedsDependency & AuthContextProvider, + dependency: UIViewController & NeedsDependency & AuthContextProvider & DataSourceProvider, action: MastodonMenu.Action, menuContext: MenuContext ) async throws { @@ -389,13 +389,15 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToToggleSensitiveAction( - dependency: NeedsDependency, + dependency: NeedsDependency & DataSourceProvider, status: MastodonStatus ) async throws { - try await dependency.context.managedObjectContext.perform { - let _status = status.reblog ?? status - _status.isSensitiveToggled = !_status.isSensitiveToggled - } + let _status = status.reblog ?? status + + let newStatus: MastodonStatus = .fromEntity(_status.entity) + newStatus.isSensitiveToggled = !_status.isSensitiveToggled + + dependency.update(status: newStatus) } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index a1c5e3925..a67da6167 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -16,6 +16,7 @@ import MastodonLocalization import CoreDataStack import TabBarPager import XLPagerTabStrip +import MastodonSDK protocol ProfileViewModelEditable { var isEdited: Bool { get } @@ -940,3 +941,14 @@ private extension ProfileViewController { authContext.mastodonAuthenticationBox.authentication.instance(in: context.managedObjectContext) } } + +extension ProfileViewController: DataSourceProvider { + func item(from source: DataSourceItem.Source) async -> DataSourceItem? { + assertionFailure("Implement not required in this class") + return nil + } + + func update(status: MastodonStatus) { + assertionFailure("Implement not required in this class") + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index 4b580c01d..ae61a65c0 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -56,13 +56,15 @@ extension MastodonFeed: Hashable { public static func == (lhs: MastodonFeed, rhs: MastodonFeed) -> Bool { lhs.id == rhs.id && lhs.status?.entity == rhs.status?.entity && - lhs.status?.reblog?.entity == rhs.status?.reblog?.entity + lhs.status?.reblog?.entity == rhs.status?.reblog?.entity && + lhs.status?.isSensitiveToggled == rhs.status?.isSensitiveToggled } public func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(status?.entity) hasher.combine(status?.reblog?.entity) + hasher.combine(status?.isSensitiveToggled) } } diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift index 71bc07b2c..3257d900a 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift @@ -37,12 +37,14 @@ extension MastodonStatus { extension MastodonStatus: Hashable { public static func == (lhs: MastodonStatus, rhs: MastodonStatus) -> Bool { lhs.entity == rhs.entity && - lhs.reblog?.entity == rhs.reblog?.entity + lhs.reblog?.entity == rhs.reblog?.entity && + lhs.isSensitiveToggled == rhs.isSensitiveToggled } public func hash(into hasher: inout Hasher) { hasher.combine(entity) hasher.combine(reblog?.entity) + hasher.combine(isSensitiveToggled) } } From 7ad0b15b329e7cf2cbd8001d43ce009a236fc5ac Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 28 Nov 2023 11:13:13 +0100 Subject: [PATCH 016/159] Fix bookmarked state (IOS-176) --- .../Provider/DataSourceFacade+Bookmark.swift | 8 +++++--- .../FeedFetchedResultsController.swift | 2 ++ .../View/Content/StatusView+ViewModel.swift | 12 +++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 70a3fdbc0..8612254f3 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -13,15 +13,17 @@ import MastodonSDK extension DataSourceFacade { public static func responseToStatusBookmarkAction( - provider: UIViewController & NeedsDependency & AuthContextProvider, + provider: UIViewController & NeedsDependency & AuthContextProvider & DataSourceProvider, status: MastodonStatus ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - _ = try await provider.context.apiService.bookmark( + let updatedStatus = try await provider.context.apiService.bookmark( record: status, authenticationBox: provider.authContext.mastodonAuthenticationBox - ) + ).value + + provider.update(status: .fromEntity(updatedStatus)) } } diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 8b630b274..3961c5bae 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -52,12 +52,14 @@ final public class FeedFetchedResultsController { 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) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 735e4c472..715008287 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -27,7 +27,16 @@ extension StatusView { public var context: AppContext? public var authContext: AuthContext? - public var originalStatus: MastodonStatus? + public var originalStatus: MastodonStatus? { + didSet { + originalStatus?.$entity + .receive(on: DispatchQueue.main) + .sink(receiveValue: { status in + self.isBookmark = status.bookmarked == true + }) + .store(in: &disposeBag) + } + } // Header @Published public var header: Header = .none @@ -148,6 +157,7 @@ extension StatusView { isMediaSensitive = false isSensitiveToggled = false isCurrentlyTranslating = false + isBookmark = false translation = nil activeFilters = [] From 66b2bfd5cdab7a6ecea85ee8cc2b0a3d8c1a43aa Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 28 Nov 2023 12:04:08 +0100 Subject: [PATCH 017/159] Fix missing case in StatusFetchedResultsController --- .../StatusFetchedResultsController.swift | 2 ++ .../Sources/MastodonUI/View/Content/StatusView+ViewModel.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 67572a766..05ef80011 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -44,6 +44,8 @@ public final class StatusFetchedResultsController { newRecords[i] = status } else if let reblog = record.reblog, reblog.id == status.id { newRecords[i].reblog = status + } else if status.reblog?.id == status.id { + newRecords[i].reblog = status } } records = newRecords diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 715008287..8ca7cd79a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -33,6 +33,7 @@ extension StatusView { .receive(on: DispatchQueue.main) .sink(receiveValue: { status in self.isBookmark = status.bookmarked == true + self.isMuting = status.muted == true }) .store(in: &disposeBag) } From 1f73bee772ce1435bb553497cd761774f3db529b Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 28 Nov 2023 12:23:14 +0100 Subject: [PATCH 018/159] Improve StatusFetchedResultsController update handling (IOS-176) --- .../StatusFetchedResultsController.swift | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 05ef80011..78bb50964 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -42,10 +42,29 @@ public final class StatusFetchedResultsController { for (i, record) in newRecords.enumerated() { if record.id == status.id { newRecords[i] = status + } else if let reblog = status.reblog, reblog.id == record.id { + newRecords[i] = status } else if let reblog = record.reblog, reblog.id == status.id { - newRecords[i].reblog = status - } else if status.reblog?.id == status.id { - newRecords[i].reblog = status + // 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 + }() + } + + } else if let reblog = record.reblog, reblog.id == status.reblog?.id { + // Handle re-reblogged state + newRecords[i] = status } } records = newRecords From 112dce06f5621382d9588793072c099ab9959b53 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 1 Dec 2023 09:52:08 +0100 Subject: [PATCH 019/159] Improve flickering when interacting with posts (IOS-176) --- .../Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index d2ea8b878..e022f92e4 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -76,12 +76,12 @@ extension HomeTimelineViewModel { oldSnapshot: oldSnapshot, newSnapshot: newSnapshot ) else { - self.updateSnapshotUsingReloadData(snapshot: newSnapshot) + await self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) self.didLoadLatest.send() return } - self.updateSnapshotUsingReloadData(snapshot: newSnapshot) + await self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) var contentOffset = tableView.contentOffset contentOffset.y = tableView.contentOffset.y - difference.sourceDistanceToTableViewTopEdge From 38423a14cab94c9e38926502eb54cb111ce21119 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 1 Dec 2023 09:52:41 +0100 Subject: [PATCH 020/159] Preserve isSensitiveToggled state when interacting with posts (IOS-176) --- Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift | 5 ++++- Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift | 7 +++++-- Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift | 7 +++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 8612254f3..5e342108b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -24,6 +24,9 @@ extension DataSourceFacade { authenticationBox: provider.authContext.mastodonAuthenticationBox ).value - provider.update(status: .fromEntity(updatedStatus)) + let newStatus: MastodonStatus = .fromEntity(updatedStatus) + newStatus.isSensitiveToggled = status.isSensitiveToggled + + provider.update(status: newStatus) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift index e2a4397e5..8e96e28fa 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -18,11 +18,14 @@ extension DataSourceFacade { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - let newStatus = try await provider.context.apiService.favorite( + let updatedStatus = try await provider.context.apiService.favorite( status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox ).value - provider.update(status: .fromEntity(newStatus)) + let newStatus: MastodonStatus = .fromEntity(updatedStatus) + newStatus.isSensitiveToggled = status.isSensitiveToggled + + provider.update(status: newStatus) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index 1d445697e..75f0f63aa 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -18,11 +18,14 @@ extension DataSourceFacade { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - let newStatus = try await provider.context.apiService.reblog( + let updatedStatus = try await provider.context.apiService.reblog( status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox ).value - provider.update(status: .fromEntity(newStatus)) + let newStatus: MastodonStatus = .fromEntity(updatedStatus) + newStatus.isSensitiveToggled = status.isSensitiveToggled + + provider.update(status: newStatus) } } From 28cdd67e8aa52bf7485e183a4dc7e846fc9af5aa Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 1 Dec 2023 09:57:25 +0100 Subject: [PATCH 021/159] Fix sensitive toggled state for reblogs (IOS-176) --- Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index 75f0f63aa..f8fa3d5f3 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -24,6 +24,7 @@ extension DataSourceFacade { ).value let newStatus: MastodonStatus = .fromEntity(updatedStatus) + newStatus.reblog?.isSensitiveToggled = status.isSensitiveToggled newStatus.isSensitiveToggled = status.isSensitiveToggled provider.update(status: newStatus) From 837d8dd329fe4503cdd7f5417a6c92531674cfcd Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 1 Dec 2023 10:14:12 +0100 Subject: [PATCH 022/159] Fix user profile not loaded from status on home timeline (IOS-176) --- .../Provider/DataSourceFacade+Profile.swift | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index bfd8e0fb7..3f2c6fe2d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -17,18 +17,29 @@ extension DataSourceFacade { target: StatusTarget, status: MastodonStatus ) async { - let _redirectRecord = await DataSourceFacade.author( - managedObjectContext: provider.context.managedObjectContext, - status: status, - target: target - ) + let acct: String = { + switch target { + case .status: + return status.reblog?.entity.account.acct ?? status.entity.account.acct + case .reblog: + return status.entity.account.acct + } + }() + + let _redirectRecord = try? await Mastodon.API.Account.lookupAccount( + session: .shared, + domain: provider.authContext.mastodonAuthenticationBox.domain, + query: .init(acct: acct), + authorization: provider.authContext.mastodonAuthenticationBox.userAuthorization + ).singleOutput().value + guard let redirectRecord = _redirectRecord else { assertionFailure() return } await coordinateToProfileScene( provider: provider, - user: redirectRecord + account: redirectRecord ) } From 5532324f83bd778db28870f59ee2c5e48773e2e8 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 1 Dec 2023 10:47:18 +0100 Subject: [PATCH 023/159] Implement caching for home timeline items (IOS-176) --- Mastodon.xcodeproj/project.pbxproj | 4 ++ Mastodon/Coordinator/SceneCoordinator.swift | 2 +- .../FileManager+HomeTimeline.swift | 53 +++++++++++++++++++ .../FileManager+SearchHistory.swift | 10 ++-- .../HomeTimelineViewController.swift | 1 + .../HomeTimeline/HomeTimelineViewModel.swift | 17 ++++++ .../Persistence/Persistence.swift | 3 ++ 7 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 Mastodon/Persistence/FileManager+HomeTimeline.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 363660d02..8862ef43c 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; }; 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; + 2AF2E7BF2B19DC6E00D98917 /* FileManager+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; @@ -696,6 +697,7 @@ 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; + 2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+HomeTimeline.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -1898,6 +1900,7 @@ children = ( D8AC98772B0F62230045EC2B /* Model */, D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */, + 2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */, ); path = Persistence; sourceTree = ""; @@ -3978,6 +3981,7 @@ DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */, DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */, + 2AF2E7BF2B19DC6E00D98917 /* FileManager+HomeTimeline.swift in Sources */, DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */, DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */, DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 400042065..47a192d22 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -626,7 +626,7 @@ extension SceneCoordinator: SettingsCoordinatorDelegate { try await self.appContext.authenticationService.signOutMastodonUser( authenticationBox: authContext.mastodonAuthenticationBox ) - + FileManager.default.invalidateHomeTimelineCache(for: authContext.mastodonAuthenticationBox.userID) self.setup() } diff --git a/Mastodon/Persistence/FileManager+HomeTimeline.swift b/Mastodon/Persistence/FileManager+HomeTimeline.swift new file mode 100644 index 000000000..de1c38e88 --- /dev/null +++ b/Mastodon/Persistence/FileManager+HomeTimeline.swift @@ -0,0 +1,53 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import MastodonCore +import MastodonSDK + +extension FileManager { + private static let cacheHomeItemsLimit: Int = 100 // max number of items to cache + + func cachedHomeTimeline(for userId: String) throws -> [MastodonStatus] { + guard let cachesDirectory else { return [] } + + let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory) + + guard let data = try? Data(contentsOf: filePath) else { return [] } + + do { + let items = try JSONDecoder().decode([Mastodon.Entity.Status].self, from: data) + + return items.map(MastodonStatus.fromEntity) + } catch { + return [] + } + } + + func cacheHomeTimeline(items: [MastodonStatus], for userId: String) { + guard let cachesDirectory else { return } + + let processableItems: [MastodonStatus] + if items.count > Self.cacheHomeItemsLimit { + processableItems = items.dropLast(items.count - Self.cacheHomeItemsLimit) + } else { + processableItems = items + } + + do { + let data = try JSONEncoder().encode(processableItems.map { $0.entity }) + + let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory) + try data.write(to: filePath) + } catch { + debugPrint(error.localizedDescription) + } + } + + func invalidateHomeTimelineCache(for userId: String) { + guard let cachesDirectory else { return } + + let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory) + + try? removeItem(at: filePath) + } +} diff --git a/Mastodon/Persistence/FileManager+SearchHistory.swift b/Mastodon/Persistence/FileManager+SearchHistory.swift index 8c1cabd1d..95dd29a52 100644 --- a/Mastodon/Persistence/FileManager+SearchHistory.swift +++ b/Mastodon/Persistence/FileManager+SearchHistory.swift @@ -68,8 +68,12 @@ extension FileManager { } } -extension FileManager { - public var documentsDirectory: URL? { - return self.urls(for: .documentDirectory, in: .userDomainMask).first +public extension FileManager { + var documentsDirectory: URL? { + urls(for: .documentDirectory, in: .userDomainMask).first + } + + var cachesDirectory: URL? { + urls(for: .cachesDirectory, in: .userDomainMask).first } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index ed363f397..cae1971d8 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -386,6 +386,7 @@ extension HomeTimelineViewController { @objc func signOutAction(_ sender: UIAction) { Task { @MainActor in try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox) + FileManager.default.invalidateHomeTimelineCache(for: viewModel.authContext.mastodonAuthenticationBox.userID) self.coordinator.setup() } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 24d3dded7..670103c5e 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -15,6 +15,7 @@ import GameplayKit import AlamofireImage import MastodonCore import MastodonUI +import MastodonSDK final class HomeTimelineViewModel: NSObject { @@ -83,6 +84,10 @@ final class HomeTimelineViewModel: NSObject { self.fetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext) self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) super.init() + self.fetchedResultsController.records = (try? FileManager.default.cachedHomeTimeline(for: authContext.mastodonAuthenticationBox.userID).map { + MastodonFeed.fromStatus($0, kind: .home) + }) ?? [] + homeTimelineNeedRefresh .sink { [weak self] _ in self?.loadLatestStateMachine.enter(LoadLatestState.Loading.self) @@ -98,6 +103,18 @@ final class HomeTimelineViewModel: NSObject { } .store(in: &disposeBag) + self.fetchedResultsController.$records + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink(receiveValue: { feeds in + let items: [MastodonStatus] = feeds.compactMap { feed -> MastodonStatus? in + guard let status = feed.status else { return nil } + return status + } + FileManager.default.cacheHomeTimeline(items: items, for: authContext.mastodonAuthenticationBox.userID) + }) + .store(in: &disposeBag) + self.fetchedResultsController.loadInitial(kind: .home) } } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index 8445d87ba..5b5090bf8 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -10,11 +10,14 @@ import Foundation public enum Persistence { case searchHistory + case homeTimeline(String) private var filename: String { switch self { case .searchHistory: return "search_history" + case let .homeTimeline(userId): + return "home_timeline_\(userId)" } } From b57f38ddc564d371c06d881c90cd52b389a3afb4 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 4 Dec 2023 09:56:48 +0100 Subject: [PATCH 024/159] Fix status eventuall nil MastodonNotification --- MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index ae61a65c0..c5016934a 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -45,7 +45,12 @@ public extension MastodonFeed { MastodonFeed( hasMore: false, isLoadingMore: false, - status: nil, + status: { + guard let status = notification.status else { + return nil + } + return .fromEntity(status) + }(), notification: notification, kind: kind ) From 63e45d65f96c349f430456bbebfa0c172fe0a580 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 4 Dec 2023 10:14:42 +0100 Subject: [PATCH 025/159] Fix notifications show empty view (IOS-198) --- ...Provider+StatusTableViewCellDelegate.swift | 4 ++- ...ineViewController+DataSourceProvider.swift | 2 +- .../NotificationTimelineViewModel.swift | 35 ------------------- .../NotificationView+Configuration.swift | 6 +++- .../Sources/MastodonSDK/MastodonFeed.swift | 2 +- .../MastodonSDK/MastodonNotification.swift | 9 +++-- .../MastodonUI/View/Content/StatusView.swift | 4 +++ 7 files changed, 20 insertions(+), 42 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 5550b177d..c0a6c2381 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -22,6 +22,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte statusView: StatusView, headerDidPressed header: UIView ) { + let domain = statusView.domain ?? "" Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -40,12 +41,13 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte let _replyToAuthor: ManagedObjectRecord? = try? await context.managedObjectContext.perform { guard let inReplyToAccountID = status.entity.inReplyToAccountID else { return nil } let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: status.entity.account.domain ?? "", id: inReplyToAccountID) + request.predicate = MastodonUser.predicate(domain: domain, id: inReplyToAccountID) request.fetchLimit = 1 guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } return .init(objectID: author.objectID) } guard let replyToAuthor = _replyToAuthor else { + assertionFailure() return } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index a15f1664c..4cda5e530 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -25,7 +25,7 @@ extension NotificationTimelineViewController: DataSourceProvider { let managedObjectContext = context.managedObjectContext let item: DataSourceItem? = { guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil } - if let notification = feed.notification, let mastodonNotification = MastodonNotification.fromEntity(notification, using: managedObjectContext) { + if let notification = feed.notification, let mastodonNotification = MastodonNotification.fromEntity(notification, using: managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain) { return .notification(record: mastodonNotification) } else { return nil diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index ca739ed55..fc4bd4557 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -61,41 +61,6 @@ final class NotificationTimelineViewModel { extension NotificationTimelineViewModel { typealias Scope = APIService.MastodonNotificationScope - - static func feedPredicate( - authenticationBox: MastodonAuthenticationBox, - scope: Scope - ) -> NSPredicate { - let domain = authenticationBox.domain - let userID = authenticationBox.userID - let acct = Feed.Acct.mastodon( - domain: domain, - userID: userID - ) - - let predicate: NSPredicate = { - switch scope { - case .everything: - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - Feed.hasNotificationPredicate(), - Feed.predicate( - kind: .notificationAll, - acct: acct - ) - ]) - case .mentions: - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - Feed.hasNotificationPredicate(), - Feed.predicate( - kind: .notificationMentions, - acct: acct - ), - Feed.notificationTypePredicate(types: scope.includeTypes ?? []) - ]) - } - }() - return predicate - } } diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index 9600461df..98dec5397 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -28,7 +28,11 @@ extension NotificationView { return } - MastodonNotification.fromEntity(notification, using: managedObjectContext).map(configure(notification:)) + MastodonNotification.fromEntity( + notification, + using: managedObjectContext, + domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? "" + ).map(configure(notification:)) } } diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index c5016934a..fbeed892f 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -21,7 +21,7 @@ public final class MastodonFeed { public let kind: Feed.Kind init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, kind: Feed.Kind) { - self.id = status?.id ?? notification?.id ?? UUID().uuidString + self.id = notification?.id ?? status?.id ?? UUID().uuidString self.hasMore = hasMore self.isLoadingMore = isLoadingMore self.status = status diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift index 4cffc393f..b32d59c29 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift @@ -26,10 +26,13 @@ public final class MastodonNotification { } public extension MastodonNotification { - static func fromEntity(_ entity: Mastodon.Entity.Notification, using managedObjectContext: NSManagedObjectContext) -> MastodonNotification? { + static func fromEntity(_ entity: Mastodon.Entity.Notification, using managedObjectContext: NSManagedObjectContext, domain: String) -> MastodonNotification? { guard let user = MastodonUser.fetch(in: managedObjectContext, configurationBlock: { request in - request.predicate = MastodonUser.predicate(domain: entity.account.domain ?? "", id: entity.account.id) - }).first else { return nil } + request.predicate = MastodonUser.predicate(domain: domain, id: entity.account.id) + }).first else { + assertionFailure() + return nil + } return MastodonNotification(entity: entity, account: user, status: entity.status.map(MastodonStatus.fromEntity), feeds: []) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 5c57db96b..bdd160435 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -50,6 +50,10 @@ public final class StatusView: UIView { public weak var delegate: StatusViewDelegate? public private(set) var style: Style? + + public var domain: String? { + viewModel.authContext?.mastodonAuthenticationBox.domain + } // accessibility actions var toolbarActions = [UIAccessibilityCustomAction]() From a1cd1690faf36707f8c4b68cee8a41a6200c2936 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 4 Dec 2023 11:04:00 +0100 Subject: [PATCH 026/159] Implement cache for Notifications (IOS-200) --- Mastodon.xcodeproj/project.pbxproj | 8 +- Mastodon/Coordinator/SceneCoordinator.swift | 5 +- .../FileManager+HomeTimeline.swift | 53 ----------- .../Persistence/FileManager+Timeline.swift | 93 +++++++++++++++++++ .../HomeTimelineViewController.swift | 5 +- .../NotificationTimelineViewModel.swift | 28 ++++++ .../Persistence/Persistence.swift | 6 ++ 7 files changed, 139 insertions(+), 59 deletions(-) delete mode 100644 Mastodon/Persistence/FileManager+HomeTimeline.swift create mode 100644 Mastodon/Persistence/FileManager+Timeline.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 8862ef43c..733f91e66 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -60,7 +60,7 @@ 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; }; 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; - 2AF2E7BF2B19DC6E00D98917 /* FileManager+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */; }; + 2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; @@ -697,7 +697,7 @@ 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; - 2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+HomeTimeline.swift"; sourceTree = ""; }; + 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Timeline.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -1900,7 +1900,7 @@ children = ( D8AC98772B0F62230045EC2B /* Model */, D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */, - 2AF2E7BE2B19DC6E00D98917 /* FileManager+HomeTimeline.swift */, + 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */, ); path = Persistence; sourceTree = ""; @@ -3981,7 +3981,7 @@ DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */, DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */, - 2AF2E7BF2B19DC6E00D98917 /* FileManager+HomeTimeline.swift in Sources */, + 2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */, DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */, DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */, DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 47a192d22..4e61c31ea 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -626,7 +626,10 @@ extension SceneCoordinator: SettingsCoordinatorDelegate { try await self.appContext.authenticationService.signOutMastodonUser( authenticationBox: authContext.mastodonAuthenticationBox ) - FileManager.default.invalidateHomeTimelineCache(for: authContext.mastodonAuthenticationBox.userID) + let userId = authContext.mastodonAuthenticationBox.userID + FileManager.default.invalidateHomeTimelineCache(for: userId) + FileManager.default.invalidateNotificationsAll(for: userId) + FileManager.default.invalidateNotificationsMentions(for: userId) self.setup() } diff --git a/Mastodon/Persistence/FileManager+HomeTimeline.swift b/Mastodon/Persistence/FileManager+HomeTimeline.swift deleted file mode 100644 index de1c38e88..000000000 --- a/Mastodon/Persistence/FileManager+HomeTimeline.swift +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright © 2023 Mastodon gGmbH. All rights reserved. - -import Foundation -import MastodonCore -import MastodonSDK - -extension FileManager { - private static let cacheHomeItemsLimit: Int = 100 // max number of items to cache - - func cachedHomeTimeline(for userId: String) throws -> [MastodonStatus] { - guard let cachesDirectory else { return [] } - - let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory) - - guard let data = try? Data(contentsOf: filePath) else { return [] } - - do { - let items = try JSONDecoder().decode([Mastodon.Entity.Status].self, from: data) - - return items.map(MastodonStatus.fromEntity) - } catch { - return [] - } - } - - func cacheHomeTimeline(items: [MastodonStatus], for userId: String) { - guard let cachesDirectory else { return } - - let processableItems: [MastodonStatus] - if items.count > Self.cacheHomeItemsLimit { - processableItems = items.dropLast(items.count - Self.cacheHomeItemsLimit) - } else { - processableItems = items - } - - do { - let data = try JSONEncoder().encode(processableItems.map { $0.entity }) - - let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory) - try data.write(to: filePath) - } catch { - debugPrint(error.localizedDescription) - } - } - - func invalidateHomeTimelineCache(for userId: String) { - guard let cachesDirectory else { return } - - let filePath = Persistence.homeTimeline(userId).filepath(baseURL: cachesDirectory) - - try? removeItem(at: filePath) - } -} diff --git a/Mastodon/Persistence/FileManager+Timeline.swift b/Mastodon/Persistence/FileManager+Timeline.swift new file mode 100644 index 000000000..e9720212c --- /dev/null +++ b/Mastodon/Persistence/FileManager+Timeline.swift @@ -0,0 +1,93 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import MastodonCore +import MastodonSDK + +extension FileManager { + private static let cacheItemsLimit: Int = 100 // max number of items to cache + + // Retrieve + func cachedHomeTimeline(for userId: String) throws -> [MastodonStatus] { + try cached(timeline: .homeTimeline(userId)).map(MastodonStatus.fromEntity) + } + + func cachedNotificationsAll(for userId: String) throws -> [Mastodon.Entity.Notification] { + try cached(timeline: .notificationsAll(userId)) + } + + func cachedNotificationsMentions(for userId: String) throws -> [Mastodon.Entity.Notification] { + try cached(timeline: .notificationsMentions(userId)) + } + + + private func cached(timeline: Persistence) throws -> [T] { + guard let cachesDirectory else { return [] } + + let filePath = timeline.filepath(baseURL: cachesDirectory) + + guard let data = try? Data(contentsOf: filePath) else { return [] } + + do { + let items = try JSONDecoder().decode([T].self, from: data) + + return items + } catch { + return [] + } + } + + // Create + func cacheHomeTimeline(items: [MastodonStatus], for userId: String) { + cache(items.map { $0.entity }, timeline: .homeTimeline(userId)) + } + + func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userId: String) { + cache(items, timeline: .notificationsAll(userId)) + } + + func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userId: String) { + cache(items, timeline: .notificationsMentions(userId)) + } + + private func cache(_ items: [T], timeline: Persistence) { + guard let cachesDirectory else { return } + + let processableItems: [T] + if items.count > Self.cacheItemsLimit { + processableItems = items.dropLast(items.count - Self.cacheItemsLimit) + } else { + processableItems = items + } + + do { + let data = try JSONEncoder().encode(processableItems) + + let filePath = timeline.filepath(baseURL: cachesDirectory) + try data.write(to: filePath) + } catch { + debugPrint(error.localizedDescription) + } + } + + // Delete + func invalidateHomeTimelineCache(for userId: String) { + invalidate(timeline: .homeTimeline(userId)) + } + + func invalidateNotificationsAll(for userId: String) { + invalidate(timeline: .notificationsAll(userId)) + } + + func invalidateNotificationsMentions(for userId: String) { + invalidate(timeline: .notificationsMentions(userId)) + } + + private func invalidate(timeline: Persistence) { + guard let cachesDirectory else { return } + + let filePath = timeline.filepath(baseURL: cachesDirectory) + + try? removeItem(at: filePath) + } +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index cae1971d8..54cc8a722 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -386,7 +386,10 @@ extension HomeTimelineViewController { @objc func signOutAction(_ sender: UIAction) { Task { @MainActor in try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox) - FileManager.default.invalidateHomeTimelineCache(for: viewModel.authContext.mastodonAuthenticationBox.userID) + let userId = viewModel.authContext.mastodonAuthenticationBox.userID + FileManager.default.invalidateHomeTimelineCache(for: userId) + FileManager.default.invalidateNotificationsAll(for: userId) + FileManager.default.invalidateNotificationsMentions(for: userId) self.coordinator.setup() } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index fc4bd4557..c18d9fdd2 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -53,6 +53,34 @@ final class NotificationTimelineViewModel { self.authContext = authContext self.scope = scope self.feedFetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext) + + switch scope { + case .everything: + self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox.userID))?.map({ notification in + MastodonFeed.fromNotification(notification, kind: .notificationAll) + }) ?? [] + case .mentions: + self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox.userID))?.map({ notification in + MastodonFeed.fromNotification(notification, kind: .notificationMentions) + }) ?? [] + } + + self.feedFetchedResultsController.$records + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink(receiveValue: { feeds in + let items: [Mastodon.Entity.Notification] = feeds.compactMap { feed -> Mastodon.Entity.Notification? in + guard let status = feed.notification else { return nil } + return status + } + switch self.scope { + case .everything: + FileManager.default.cacheNotificationsAll(items: items, for: authContext.mastodonAuthenticationBox.userID) + case .mentions: + FileManager.default.cacheNotificationsMentions(items: items, for: authContext.mastodonAuthenticationBox.userID) + } + }) + .store(in: &disposeBag) } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index 5b5090bf8..1e3090c2e 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -11,6 +11,8 @@ import Foundation public enum Persistence { case searchHistory case homeTimeline(String) + case notificationsMentions(String) + case notificationsAll(String) private var filename: String { switch self { @@ -18,6 +20,10 @@ public enum Persistence { return "search_history" case let .homeTimeline(userId): return "home_timeline_\(userId)" + case let .notificationsMentions(userId): + return "notifications_mentions_\(userId)" + case let .notificationsAll(userId): + return "notifications_all_\(userId)" } } From 69a92c3d0fef128a6a885fbe0cc30941246248e6 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 4 Dec 2023 11:09:47 +0100 Subject: [PATCH 027/159] Fix eventually incorrect domain used --- .../Provider/DataSourceFacade+Model.swift | 16 ---------------- .../NotificationTimelineViewController.swift | 3 ++- .../View/Content/StatusView+Configuration.swift | 9 --------- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift index 9364cbc24..42e44d832 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift @@ -25,19 +25,3 @@ extension DataSourceFacade { } } } - -extension DataSourceFacade { - static func author( - managedObjectContext: NSManagedObjectContext, - status: MastodonStatus, - target: StatusTarget - ) async -> ManagedObjectRecord? { - return try? await managedObjectContext.perform { - return DataSourceFacade.status(managedObjectContext: managedObjectContext, status: status, target: target) - .flatMap { $0.entity.account } - .flatMap { - MastodonUser.findOrFetch(in: managedObjectContext, matching: MastodonUser.predicate(domain: $0.domain ?? "", id: $0.id))?.asRecord - } - } - } -} diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index d4c68c09f..053ce9e76 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -276,6 +276,7 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return } guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return } + let domain = authContext.mastodonAuthenticationBox.domain Task { @MainActor in switch item { @@ -296,7 +297,7 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { } else { context.managedObjectContext.perform { let mastodonUserRequest = MastodonUser.sortedFetchRequest - mastodonUserRequest.predicate = MastodonUser.predicate(domain: notification.account.domain ?? "", id: notification.account.id) + mastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: notification.account.id) mastodonUserRequest.fetchLimit = 1 guard let mastodonUser = try? self.context.managedObjectContext.fetch(mastodonUserRequest).first else { return diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 245521d3d..2b62211f7 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -142,15 +142,6 @@ extension StatusView { } } else { // B. replyTo status not exist - -// let request = MastodonUser.sortedFetchRequest -// request.predicate = MastodonUser.predicate(domain: status.domain, id: inReplyToAccountID) -// if let user = status.managedObjectContext?.safeFetch(request).first { -// // B1. replyTo user exist -// let header = createHeader(name: user.displayNameWithFallback, emojis: user.emojis.asDictionary) -// viewModel.header = header -// } else { - // B2. replyTo user not exist let header = createHeader(name: nil, emojis: nil) viewModel.header = header From 7323cb9d08f8ae65df374296f72bea385c9038b5 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 4 Dec 2023 11:48:48 +0100 Subject: [PATCH 028/159] Fix missing poll information (IOS-201) --- .../Scene/ComposeContent/ComposeContentViewModel.swift | 2 +- .../View/Content/StatusView+Configuration.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index cac5b71da..24e34afe3 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -284,7 +284,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { contentWarning = status.entity.spoilerText ?? "" } Task { - if let poll = await status.getPoll(in: context.managedObjectContext) { + if let poll = await status.getPoll(in: context.managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain) { isPollActive = !poll.expired pollMultipleConfigurationOption = poll.multiple if let pollExpiresAt = poll.expiresAt { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 2b62211f7..a9eab1f99 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -362,12 +362,13 @@ extension StatusView { private func configurePoll(status: MastodonStatus) { Task { + let status = status.reblog ?? status + guard let context = viewModel.context?.managedObjectContext, - let poll = await status.getPoll(in: context) + let poll = await status.getPoll(in: context, domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? "") else { return } - let status = status.reblog ?? status viewModel.managedObjects.insert(poll) @@ -507,9 +508,8 @@ extension StatusView { } extension MastodonStatus { - func getPoll(in context: NSManagedObjectContext) async -> Poll? { + func getPoll(in context: NSManagedObjectContext, domain: String) async -> Poll? { guard - let domain = entity.account.domain, let pollId = entity.poll?.id else { return nil } return try? await context.perform { From 0f3c50ba8af13928efef781e3499fb510e9f1d79 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 4 Dec 2023 14:31:38 +0100 Subject: [PATCH 029/159] Fix non-ui-thread modification in when editing poll (IOS-203) --- Mastodon/Diffable/Status/StatusSection.swift | 4 +--- .../Scene/ComposeContent/ComposeContentViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Mastodon/Diffable/Status/StatusSection.swift b/Mastodon/Diffable/Status/StatusSection.swift index d6dbeccf6..d9c1b4725 100644 --- a/Mastodon/Diffable/Status/StatusSection.swift +++ b/Mastodon/Diffable/Status/StatusSection.swift @@ -109,9 +109,7 @@ extension StatusSection { tableView: UITableView, indexPath: IndexPath, configuration: ThreadCellRegistrationConfiguration - ) -> UITableViewCell { - let managedObjectContext = context.managedObjectContext - + ) -> UITableViewCell { switch configuration.thread { case .root(let threadContext): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 24e34afe3..5539f80ee 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -283,7 +283,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { isContentWarningActive = true contentWarning = status.entity.spoilerText ?? "" } - Task { + Task { @MainActor in if let poll = await status.getPoll(in: context.managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain) { isPollActive = !poll.expired pollMultipleConfigurationOption = poll.multiple From 86d30887b6e573d26a9ef94c6e656cca04b0822b Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Mon, 4 Dec 2023 14:36:31 +0100 Subject: [PATCH 030/159] Remove unused code --- .../Service/API/APIService+Bookmark.swift | 27 ++++------------ .../Service/API/APIService+Favorite.swift | 29 +++++------------ .../Service/API/APIService+Reblog.swift | 32 ++++++------------- 3 files changed, 24 insertions(+), 64 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift index 4f31c91bc..75953c17f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift @@ -22,29 +22,16 @@ extension APIService { record: MastodonStatus, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - - let managedObjectContext = backgroundManagedObjectContext // update bookmark state and retrieve bookmark context - let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let me = authentication.user(in: managedObjectContext) - else { - throw APIError.implicit(.badRequest) - } - - let _status = record.entity - let status = _status.reblog ?? _status - let isBookmarked = status.bookmarked == true + let _status = record.entity + let status = _status.reblog ?? _status + let isBookmarked = status.bookmarked == true - let context = MastodonBookmarkContext( - statusID: status.id, - isBookmarked: isBookmarked - ) - return context - } + let bookmarkContext = MastodonBookmarkContext( + statusID: status.id, + isBookmarked: isBookmarked + ) // request bookmark or undo bookmark let result: Result, Error> diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index a1e8371b6..182ffea6f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -23,30 +23,17 @@ extension APIService { status: MastodonStatus, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - - let managedObjectContext = backgroundManagedObjectContext // update like state and retrieve like context - let favoriteContext: MastodonFavoriteContext = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let me = authentication.user(in: managedObjectContext) - else { - throw APIError.implicit(.badRequest) - } + let _status = status.reblog ?? status + let isFavorited = _status.entity.favourited == true + let favoritedCount = Int64(_status.entity.favouritesCount) - let _status = status.reblog ?? status - let isFavorited = _status.entity.favourited == true - let favoritedCount = Int64(_status.entity.favouritesCount) - - let context = MastodonFavoriteContext( - statusID: _status.id, - isFavorited: isFavorited, - favoritedCount: favoritedCount - ) - return context - } + let favoriteContext = MastodonFavoriteContext( + statusID: _status.id, + isFavorited: isFavorited, + favoritedCount: favoritedCount + ) // request like or undo like let result: Result, Error> diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index eea8b7fbf..09ca59a16 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -23,30 +23,17 @@ extension APIService { status: MastodonStatus, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext - + // update repost state and retrieve repost context - let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let me = authentication.user(in: managedObjectContext) - else { return nil } - - let _status = status.reblog ?? status - let isReblogged = _status.entity.reblogged == true - let rebloggedCount = Int64(_status.entity.reblogsCount) + let _status = status.reblog ?? status + let isReblogged = _status.entity.reblogged == true + let rebloggedCount = Int64(_status.entity.reblogsCount) - let reblogContext = MastodonReblogContext( - statusID: _status.id, - isReblogged: isReblogged, - rebloggedCount: rebloggedCount - ) - return reblogContext - } - guard let reblogContext = _reblogContext else { - throw APIError.implicit(.badRequest) - } + let reblogContext = MastodonReblogContext( + statusID: _status.id, + isReblogged: isReblogged, + rebloggedCount: rebloggedCount + ) // request repost or undo repost let result: Result, Error> @@ -86,7 +73,6 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - #warning("Is this still required?") try await managedObjectContext.performChanges { for entity in response.value { _ = Persistence.MastodonUser.createOrMerge( From a4db8a2b90cdcd6c1c5fcca76a8edc003c41cc46 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 5 Dec 2023 11:52:47 +0100 Subject: [PATCH 031/159] Fix Poll appearance / layout / issues (IOS-201) --- .../Content/StatusView+Configuration.swift | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index a9eab1f99..36bc4e398 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -361,69 +361,69 @@ extension StatusView { } private func configurePoll(status: MastodonStatus) { - Task { - let status = status.reblog ?? status + let status = status.reblog ?? status - guard - let context = viewModel.context?.managedObjectContext, - let poll = await status.getPoll(in: context, domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? "") - else { return } - - - viewModel.managedObjects.insert(poll) + let predicate = Poll.predicate(domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? "", id: status.entity.poll?.id ?? "") - // pollItems - let options = poll.options.sorted(by: { $0.index < $1.index }) - let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) } - self.viewModel.pollItems = items - - // isVoteButtonEnabled - poll.publisher(for: \.updatedAt) - .sink { [weak self] _ in - guard let self = self else { return } - let options = poll.options - let hasSelectedOption = options.contains(where: { $0.isSelected }) - self.viewModel.isVoteButtonEnabled = hasSelectedOption - } - .store(in: &disposeBag) - // isVotable - Publishers.CombineLatest( - poll.publisher(for: \.votedBy), - poll.publisher(for: \.expired) - ) - .map { [weak viewModel] votedBy, expired in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - let domain = authContext.mastodonAuthenticationBox.domain - let userID = authContext.mastodonAuthenticationBox.userID - let isVoted = votedBy?.contains(where: { $0.domain == domain && $0.id == userID }) ?? false - return !isVoted && !expired + guard + let context = viewModel.context?.managedObjectContext, + let poll = Poll.findOrFetch(in: context, matching: predicate) + else { return } + + + viewModel.managedObjects.insert(poll) + + // pollItems + let options = poll.options.sorted(by: { $0.index < $1.index }) + let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) } + self.viewModel.pollItems = items + + // isVoteButtonEnabled + poll.publisher(for: \.updatedAt) + .sink { [weak self] _ in + guard let self = self else { return } + let options = poll.options + let hasSelectedOption = options.contains(where: { $0.isSelected }) + self.viewModel.isVoteButtonEnabled = hasSelectedOption } - .assign(to: &viewModel.$isVotable) - - // votesCount - poll.publisher(for: \.votesCount) - .map { Int($0) } - .assign(to: \.voteCount, on: viewModel) - .store(in: &disposeBag) - // voterCount - poll.publisher(for: \.votersCount) - .map { Int($0) } - .assign(to: \.voterCount, on: viewModel) - .store(in: &disposeBag) - // expireAt - poll.publisher(for: \.expiresAt) - .assign(to: \.expireAt, on: viewModel) - .store(in: &disposeBag) - // expired + .store(in: &disposeBag) + // isVotable + Publishers.CombineLatest( + poll.publisher(for: \.votedBy), poll.publisher(for: \.expired) - .assign(to: \.expired, on: viewModel) - .store(in: &disposeBag) - // isVoting - poll.publisher(for: \.isVoting) - .assign(to: \.isVoting, on: viewModel) - .store(in: &disposeBag) + ) + .map { [weak viewModel] votedBy, expired in + guard let viewModel = viewModel else { return false } + guard let authContext = viewModel.authContext else { return false } + let domain = authContext.mastodonAuthenticationBox.domain + let userID = authContext.mastodonAuthenticationBox.userID + let isVoted = votedBy?.contains(where: { $0.domain == domain && $0.id == userID }) ?? false + return !isVoted && !expired } + .assign(to: &viewModel.$isVotable) + + // votesCount + poll.publisher(for: \.votesCount) + .map { Int($0) } + .assign(to: \.voteCount, on: viewModel) + .store(in: &disposeBag) + // voterCount + poll.publisher(for: \.votersCount) + .map { Int($0) } + .assign(to: \.voterCount, on: viewModel) + .store(in: &disposeBag) + // expireAt + poll.publisher(for: \.expiresAt) + .assign(to: \.expireAt, on: viewModel) + .store(in: &disposeBag) + // expired + poll.publisher(for: \.expired) + .assign(to: \.expired, on: viewModel) + .store(in: &disposeBag) + // isVoting + poll.publisher(for: \.isVoting) + .assign(to: \.isVoting, on: viewModel) + .store(in: &disposeBag) } private func configureCard(status: MastodonStatus) { From c30fb73922ea77b1f003b1feec47bbd239f0aabc Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 5 Dec 2023 12:28:36 +0100 Subject: [PATCH 032/159] Fix status deletion not updating data model (IOS-205) --- Mastodon/Protocol/Provider/DataSourceFacade+Status.swift | 8 +++++--- Mastodon/Protocol/Provider/DataSourceProvider.swift | 1 + ...coveryCommunityViewController+DataSourceProvider.swift | 8 +++++++- .../DiscoveryPostsViewController+DataSourceProvider.swift | 8 +++++++- ...HashtagTimelineViewController+DataSourceProvider.swift | 4 ++++ .../HomeTimelineViewController+DataSourceProvider.swift | 6 +++++- ...icationTimelineViewController+DataSourceProvider.swift | 6 +++++- .../BookmarkViewController+DataSourceProvider.swift | 8 +++++++- .../FamiliarFollowersViewController.swift | 6 +++++- .../FavoriteViewController+DataSourceProvider.swift | 8 +++++++- .../Profile/Follower/FollowerListViewController.swift | 8 ++++++-- .../Profile/Following/FollowingListViewController.swift | 6 +++++- Mastodon/Scene/Profile/ProfileViewController.swift | 8 ++++++-- .../UserTimelineViewController+DataSourceProvider.swift | 4 ++++ .../FavoritedByViewController+DataSourceProvider.swift | 6 +++++- .../RebloggedByViewController+DataSourceProvider.swift | 6 +++++- .../SearchHistoryViewController+DataSourceProvider.swift | 6 +++++- .../SearchResultViewController+DataSourceProvider.swift | 4 ++++ .../Thread/ThreadViewController+DataSourceProvider.swift | 4 ++++ .../FeedFetchedResultsController.swift | 4 ++++ .../StatusFetchedResultsController.swift | 5 +++++ MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift | 5 ++++- 22 files changed, 110 insertions(+), 19 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 70fd66e7c..df8f634c6 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -20,13 +20,15 @@ import MastodonSDK extension DataSourceFacade { static func responseToDeleteStatus( - dependency: NeedsDependency & AuthContextProvider, + dependency: NeedsDependency & AuthContextProvider & DataSourceProvider, status: MastodonStatus ) async throws { - _ = try await dependency.context.apiService.deleteStatus( + let deletedStatus = try await dependency.context.apiService.deleteStatus( status: status, authenticationBox: dependency.authContext.mastodonAuthenticationBox - ) + ).value.asMastodonStatus + + dependency.delete(status: deletedStatus) } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index c74a16356..d3b18240e 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -40,4 +40,5 @@ extension DataSourceItem { protocol DataSourceProvider: ViewControllerWithDependencies { func item(from source: DataSourceItem.Source) async -> DataSourceItem? func update(status: MastodonStatus) + func delete(status: MastodonStatus) } diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift index 44db77f5b..a7f6af19d 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController+DataSourceProvider.swift @@ -27,11 +27,17 @@ extension DiscoveryCommunityViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { viewModel.statusFetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.statusFetchedResultsController.setRecords( + viewModel.statusFetchedResultsController.records.filter { $0.id != status.id } + ) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift index 300235ded..f160b0d5d 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController+DataSourceProvider.swift @@ -27,11 +27,17 @@ extension DiscoveryPostsViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { viewModel.statusFetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.statusFetchedResultsController.setRecords( + viewModel.statusFetchedResultsController.records.filter { $0.id != status.id } + ) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift index 21bf2c79e..9f89d3058 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController+DataSourceProvider.swift @@ -32,6 +32,10 @@ extension HashtagTimelineViewController: DataSourceProvider { viewModel.fetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.fetchedResultsController.deleteRecord(status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift index 9636ac6b4..28b6a4984 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift @@ -35,11 +35,15 @@ extension HomeTimelineViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { viewModel.fetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.fetchedResultsController.records = viewModel.fetchedResultsController.records.filter { $0.id != status.id } + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index 4cda5e530..4a7d22229 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -36,11 +36,15 @@ extension NotificationTimelineViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { viewModel.feedFetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.feedFetchedResultsController + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift index 7b0252f62..0b3c1bc3a 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController+DataSourceProvider.swift @@ -27,11 +27,17 @@ extension BookmarkViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { viewModel.statusFetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.statusFetchedResultsController.setRecords( + viewModel.statusFetchedResultsController.records.filter { $0.id != status.id } + ) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift index f477b0599..5db16d519 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewController.swift @@ -104,7 +104,11 @@ extension FamiliarFollowersViewController: DataSourceProvider { } func update(status: MastodonStatus) { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") + } + + func delete(status: MastodonStatus) { + assertionFailure("Not required") } @MainActor diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift index 7fa1ced50..671a0d1e3 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController+DataSourceProvider.swift @@ -27,11 +27,17 @@ extension FavoriteViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { viewModel.statusFetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.statusFetchedResultsController.setRecords( + viewModel.statusFetchedResultsController.records.filter { $0.id != status.id } + ) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index 48c66f36a..404eae6fa 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -153,9 +153,13 @@ extension FollowerListViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") + } + + func delete(status: MastodonStatus) { + assertionFailure("Not required") } @MainActor diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index 5bb36a9fe..f1bd37ae7 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -151,7 +151,11 @@ extension FollowingListViewController: DataSourceProvider { } func update(status: MastodonStatus) { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") + } + + func delete(status: MastodonStatus) { + assertionFailure("Not required") } @MainActor diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index a67da6167..8ece9ac2b 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -944,11 +944,15 @@ private extension ProfileViewController { extension ProfileViewController: DataSourceProvider { func item(from source: DataSourceItem.Source) async -> DataSourceItem? { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") return nil } func update(status: MastodonStatus) { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") + } + + func delete(status: MastodonStatus) { + assertionFailure("Not required") } } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift index 3c536888d..e569c4d69 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController+DataSourceProvider.swift @@ -32,6 +32,10 @@ extension UserTimelineViewController: DataSourceProvider { viewModel.statusFetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.statusFetchedResultsController.deleteRecord(status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift index 4fbde2165..4f9580fbd 100644 --- a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift @@ -29,7 +29,11 @@ extension FavoritedByViewController: DataSourceProvider { } func update(status: MastodonStatus) { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") + } + + func delete(status: MastodonStatus) { + assertionFailure("Not required") } @MainActor diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift index c111d5491..61387c044 100644 --- a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift @@ -30,7 +30,11 @@ extension RebloggedByViewController: DataSourceProvider { } func update(status: MastodonStatus) { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") + } + + func delete(status: MastodonStatus) { + assertionFailure("Not required") } @MainActor diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift index 61c31a678..1e1a03d5f 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift @@ -30,7 +30,11 @@ extension SearchHistoryViewController: DataSourceProvider { } func update(status: MastodonStatus) { - assertionFailure("Implement not required in this class") + assertionFailure("Not required") + } + + func delete(status: MastodonStatus) { + assertionFailure("Not required") } @MainActor diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index adb84b396..7a332ad62 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -37,6 +37,10 @@ extension SearchResultViewController: DataSourceProvider { viewModel.statusFetchedResultsController.update(status: status) } + func delete(status: MastodonStatus) { + viewModel.statusFetchedResultsController.deleteRecord(status) + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift index 3ea91d0d5..187885175 100644 --- a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift @@ -33,6 +33,10 @@ extension ThreadViewController: DataSourceProvider { viewModel.root = .root(context: .init(status: status)) } + func delete(status: MastodonStatus) { + assertionFailure("Needs implementation") + } + @MainActor private func indexPath(for cell: UITableViewCell) async -> IndexPath? { return tableView.indexPath(for: cell) diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 3961c5bae..77bf8098a 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -71,6 +71,10 @@ final public class FeedFetchedResultsController { } records = newRecords } + + public func delete(status: MastodonStatus) { + self.records.removeAll { $0.id == status.id } + } } private extension FeedFetchedResultsController { diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift index 78bb50964..f0fbad458 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -36,6 +36,11 @@ public final class StatusFetchedResultsController { self.records += records } + @MainActor + public func deleteRecord(_ record: MastodonStatus) { + self.records = self.records.filter { $0.id != record.id } + } + @MainActor public func update(status: MastodonStatus) { var newRecords = Array(records) diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift index 3257d900a..ab4103eaa 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonStatus.swift @@ -49,9 +49,12 @@ extension MastodonStatus: Hashable { } public extension Mastodon.Entity.Status { + var asMastodonStatus: MastodonStatus { + .fromEntity(self) + } + var mastodonVisibility: MastodonVisibility? { guard let visibility = visibility?.rawValue else { return nil } return MastodonVisibility(rawValue: visibility) } } - From 1754375644016f974e56bcd688bc8ba511e47be6 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 7 Dec 2023 15:16:13 +0100 Subject: [PATCH 033/159] Fix favorited post might lose repost header (IOS-206) --- .../Service/API/APIService+Bookmark.swift | 30 +------- .../Service/API/APIService+Favorite.swift | 27 +------- .../Service/API/APIService+HomeTimeline.swift | 69 +------------------ .../Content/StatusView+Configuration.swift | 29 +++++++- 4 files changed, 29 insertions(+), 126 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift index 75953c17f..c535135a6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift @@ -72,35 +72,7 @@ extension APIService { authorization: authenticationBox.userAuthorization, query: query ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - - guard - let me = authenticationBox.authentication.user(in: managedObjectContext) - else { - assertionFailure() - return - } - - for entity in response.value { - let result = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: authenticationBox.domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - - result.status.update(bookmarked: true, by: me) - result.status.reblog?.update(bookmarked: true, by: me) - } // end for … in - } - + return response } // end func } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index 182ffea6f..104af8fed 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -70,32 +70,7 @@ extension APIService { authorization: authenticationBox.userAuthorization, query: query ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { - assertionFailure() - return - } - - for entity in response.value { - let result = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: authenticationBox.domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - - result.status.update(liked: true, by: me) - result.status.reblog?.update(liked: true, by: me) - } // end for … in - } - + return response } // end func } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index f806f856f..047fe1337 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -41,8 +41,6 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - // FIXME: This is a dirty hack to make the performance-stuff work. // Problem is, that we don't persist the user on disk anymore. So we have to fetch // it when we need it to display on the home timeline. @@ -54,72 +52,7 @@ extension APIService { } NotificationCenter.default.post(name: .userFetched, object: nil) - - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { - assertionFailure() - return - } - - // persist status - var statuses: [Status] = [] - for entity in response.value { - let result = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, // TODO: add cache - userCache: nil, // TODO: add cache - networkDate: response.networkDate - ) - ) - statuses.append(result.status) - } - - // locate anchor status - let anchorStatus: Status? = { - guard let maxID = maxID else { return nil } - let request = Status.sortedFetchRequest - request.predicate = Status.predicate(domain: domain, id: maxID) - request.fetchLimit = 1 - return try? managedObjectContext.fetch(request).first - }() - - // update hasMore flag for anchor status - let acct = Feed.Acct.mastodon(domain: authenticationBox.domain, userID: authenticationBox.userID) - if let anchorStatus = anchorStatus, - let feed = anchorStatus.feed(kind: .home, acct: acct) { - feed.update(hasMore: false) - } - - // persist Feed relationship - let sortedStatuses = statuses.sorted(by: { $0.createdAt < $1.createdAt }) - let oldestStatus = sortedStatuses.first - for status in sortedStatuses { - let _feed = status.feed(kind: .home, acct: acct) - if let feed = _feed { - feed.update(updatedAt: response.networkDate) - } else { - let feedProperty = Feed.Property( - acct: acct, - kind: .home, - hasMore: false, - createdAt: status.createdAt, - updatedAt: response.networkDate - ) - let feed = Feed.insert(into: managedObjectContext, property: feedProperty) - status.attach(feed: feed) - - // set hasMore on oldest status if is new feed - if status === oldestStatus { - feed.update(hasMore: true) - } - } - } - } - + return response } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 36bc4e398..cf3061c49 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -86,12 +86,35 @@ extension StatusView { extension StatusView { private func configureHeader(status: MastodonStatus) { - if let _ = status.reblog { - let name = status.entity.account.displayName + if status.entity.reblogged == true, + let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox, + let managedObjectContext = viewModel.context?.managedObjectContext { + + let user = MastodonUser.findOrFetch( + in: managedObjectContext, + matching: MastodonUser.predicate(domain: authenticationBox.domain, id: authenticationBox.userID) + ) + + let name = user?.displayNameWithFallback ?? authenticationBox.authentication.username + let emojis = user?.emojis ?? [] + + viewModel.header = { + let text = L10n.Common.Controls.Status.userReblogged(name) + let content = MastodonContent(content: text, emojis: emojis.asDictionary) + do { + let metaContent = try MastodonMetaContent.convert(document: content) + return .repost(info: .init(header: metaContent)) + } catch { + let metaContent = PlaintextMetaContent(string: name) + return .repost(info: .init(header: metaContent)) + } + }() + } 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(status.entity.account.displayNameWithFallback) + let text = L10n.Common.Controls.Status.userReblogged(name) let content = MastodonContent(content: text, emojis: emojis.asDictionary) do { let metaContent = try MastodonMetaContent.convert(document: content) From d1ba457a2fd7593aca21c15c812d47b5cb5dec99 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 7 Dec 2023 15:16:40 +0100 Subject: [PATCH 034/159] Remove status persistence (IOS-176) --- .../API/APIService+HashtagTimeline.swift | 19 ---------------- .../API/APIService+PublicTimeline.swift | 20 +---------------- .../Service/API/APIService+Search.swift | 16 +------------- .../API/APIService+Status+Publish.swift | 20 +---------------- .../Service/API/APIService+Status.swift | 18 +-------------- .../Service/API/APIService+Thread.swift | 22 +------------------ .../Service/API/APIService+Trend.swift | 19 +--------------- .../Service/API/APIService+UserTimeline.swift | 20 +---------------- 8 files changed, 7 insertions(+), 147 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index c8d1bfb73..4edd34bf3 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -41,25 +41,6 @@ extension APIService { hashtag: hashtag, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - - for entity in response.value { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - } return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift index 67d77463a..9617c5233 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift @@ -26,25 +26,7 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - for entity in response.value { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - } - + return response } // end func diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift index df607df93..123c01d17 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift @@ -41,21 +41,7 @@ extension APIService { ) ) } - - // statuses - for entity in response.value.statuses { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } + } // ent try await managedObjectContext.performChanges { … } return response diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift index 0f4896949..7536118f0 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift @@ -29,25 +29,7 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - #if !APP_EXTENSION - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: response.value, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - #endif - + return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift index 6e1d6055d..22f4b0d81 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -26,23 +26,7 @@ extension APIService { statusID: statusID, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: response.value, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - + return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index 7006a7477..293e8ccb9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -26,27 +26,7 @@ extension APIService { statusID: statusID, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - let value = response.value.ancestors + response.value.descendants - - for entity in value { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - } - + return response } // end func } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift index 06e92874e..a20c78541 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift @@ -37,24 +37,7 @@ extension APIService { query: query, authorization: authenticationBox.userAuthorization ).singleOutput() - - let managedObjectContext = backgroundManagedObjectContext - try await managedObjectContext.performChanges { - for entity in response.value { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: nil, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } // end for … in - } - + return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift index b661c282b..669e1636d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift @@ -42,25 +42,7 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - for entity in response.value { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - } - + return response } // end func From b0bdaac8b45c7740431f92ce03f4bd3497a6c05b Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 8 Dec 2023 14:29:01 +0100 Subject: [PATCH 035/159] Fix CW layout issues (IOS-207) --- .../View/Content/StatusView+Configuration.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index cf3061c49..24cf83f70 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -150,8 +150,12 @@ extension StatusView { if let inReplyToID = status.entity.inReplyToID { // A. replyTo status exist + + /// we need to initially set an empty header, otherwise the layout gets messed up + viewModel.header = createHeader(name: "", emojis: [:]) + /// finally we can load the status information and display the correct header if let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox { - Task { + Task { @MainActor in if let replyTo = try? await Mastodon.API.Statuses.status( session: .shared, domain: authenticationBox.domain, @@ -198,7 +202,7 @@ extension StatusView { } public func configureAuthor(author: Mastodon.Entity.Account) { - Task { + Task { @MainActor in // author avatar viewModel.authorAvatarImageURL = author.avatarImageURL() From ab689d3c0295b42ce146b9286d5ba1bae0c2cef9 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Fri, 8 Dec 2023 15:45:40 +0100 Subject: [PATCH 036/159] Fix status thread CW not shown and interacted status not updated (IOS-208, IOS-210) --- ...eadViewController+DataSourceProvider.swift | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift index 187885175..14b5f18a2 100644 --- a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift @@ -30,7 +30,33 @@ extension ThreadViewController: DataSourceProvider { } func update(status: MastodonStatus) { - viewModel.root = .root(context: .init(status: status)) + switch viewModel.root { + case let .root(context): + if context.status.id == status.id { + viewModel.root = .root(context: .init(status: status)) + } else { + handle(status: status) + } + case let .reply(context): + if context.status.id == status.id { + viewModel.root = .reply(context: .init(status: status)) + } else { + handle(status: status) + } + case let .leaf(context): + if context.status.id == status.id { + viewModel.root = .leaf(context: .init(status: status)) + } else { + handle(status: status) + } + case .none: + assertionFailure("This should not have happened") + } + } + + private func handle(status: MastodonStatus) { + viewModel.mastodonStatusThreadViewModel.ancestors.handle(status: status, for: viewModel) + viewModel.mastodonStatusThreadViewModel.descendants.handle(status: status, for: viewModel) } func delete(status: MastodonStatus) { @@ -42,3 +68,41 @@ extension ThreadViewController: DataSourceProvider { return tableView.indexPath(for: cell) } } + +private extension [StatusItem] { + mutating func handle(status: MastodonStatus, for viewModel: ThreadViewModel) { + for (index, ancestor) in enumerated() { + switch ancestor { + case let .feed(record): + if record.status?.id == status.id { + self[index] = .feed(record: .fromStatus(status, kind: record.kind)) + } + case let.feedLoader(record): + if record.status?.id == status.id { + self[index] = .feedLoader(record: .fromStatus(status, kind: record.kind)) + } + case let .status(record): + if record.id == status.id { + self[index] = .status(record: status) + } + case let .thread(thread): + switch thread { + case let .root(context): + if context.status.id == status.id { + self[index] = .thread(.root(context: .init(status: status))) + } + case let .reply(context): + if context.status.id == status.id { + self[index] = .thread(.reply(context: .init(status: status))) + } + case let .leaf(context): + if context.status.id == status.id { + self[index] = .thread(.leaf(context: .init(status: status))) + } + } + case .bottomLoader, .topLoader: + break + } + } + } +} From 7fb8fcaca11c6b438fa4206ed2bca5c0f1e0955d Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 12 Dec 2023 08:56:28 +0100 Subject: [PATCH 037/159] Fix issue caused by duplicate IDs after reloadingtimeline --- .../Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index e022f92e4..e15d5c4a1 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -45,7 +45,7 @@ extension HomeTimelineViewModel { var newSnapshot: NSDiffableDataSourceSnapshot = { let newItems = records.map { record in StatusItem.feed(record: record) - } + }.removingDuplicates() var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(newItems, toSection: .main) From 31796f16806239394a81ecfebd86cfe6ffebdaac Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 13 Dec 2023 15:09:37 +0100 Subject: [PATCH 038/159] Implement thread reload after publishing reply (IOS-176) --- Mastodon.xcodeproj/project.pbxproj | 4 -- .../Provider/DataSourceFacade+Meta.swift | 4 +- .../Provider/DataSourceFacade+Model.swift | 3 +- .../Provider/DataSourceFacade+Thread.swift | 6 +-- .../Scene/Thread/CachedThreadViewModel.swift | 21 --------- .../MastodonStatusThreadViewModel.swift | 4 +- ...eadViewController+DataSourceProvider.swift | 45 +++++++++++++++++-- .../ThreadViewModel+LoadThreadState.swift | 6 ++- Mastodon/Scene/Thread/ThreadViewModel.swift | 11 ++++- 9 files changed, 60 insertions(+), 44 deletions(-) delete mode 100644 Mastodon/Scene/Thread/CachedThreadViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 733f91e66..e08f34198 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -394,7 +394,6 @@ DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */; }; DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */; }; DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */; }; - DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */; }; DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */; }; DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; }; DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; }; @@ -1117,7 +1116,6 @@ DB9282B125F3222800823B15 /* PickServerEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerEmptyStateView.swift; sourceTree = ""; }; DB938EE52623F50700E5B6C1 /* ThreadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewController.swift; sourceTree = ""; }; DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = ""; }; - DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedThreadViewModel.swift; sourceTree = ""; }; DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = ""; }; DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = ""; }; DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; @@ -2690,7 +2688,6 @@ DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */, DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */, DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */, - DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */, DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */, DB0FCB7F27968F70006C02E2 /* MastodonStatusThreadViewModel.swift */, ); @@ -3912,7 +3909,6 @@ 2AB5011C299243FB00346092 /* WidgetExtension.intentdefinition in Sources */, DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, - DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift index 3cb21fb0f..5140639fd 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift @@ -19,12 +19,10 @@ extension DataSourceFacade { status: MastodonStatus, meta: Meta ) async throws { - let _redirectRecord = DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, + let redirectRecord = DataSourceFacade.status( status: status, target: target ) - guard let redirectRecord = _redirectRecord else { return } await responseToMetaTextAction( provider: provider, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift index 42e44d832..d5deef32d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift @@ -13,10 +13,9 @@ import MastodonSDK extension DataSourceFacade { static func status( - managedObjectContext: NSManagedObjectContext, status: MastodonStatus, target: StatusTarget - ) -> MastodonStatus? { + ) -> MastodonStatus { switch target { case .status: return status.reblog ?? status diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift index 61075d436..169acc073 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift @@ -18,13 +18,11 @@ extension DataSourceFacade { status: MastodonStatus ) async { let _root: StatusItem.Thread? = { - let _redirectRecord = DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, + let redirectRecord = DataSourceFacade.status( status: status, target: target ) - guard let redirectRecord = _redirectRecord else { return nil } - + let threadContext = StatusItem.Thread.Context(status: redirectRecord) return StatusItem.Thread.root(context: threadContext) }() diff --git a/Mastodon/Scene/Thread/CachedThreadViewModel.swift b/Mastodon/Scene/Thread/CachedThreadViewModel.swift deleted file mode 100644 index 9301d1876..000000000 --- a/Mastodon/Scene/Thread/CachedThreadViewModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// CachedThreadViewModel.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-4-12. -// - -import Foundation -import MastodonSDK -import MastodonCore - -final class CachedThreadViewModel: ThreadViewModel { - init(context: AppContext, authContext: AuthContext, status: MastodonStatus) { - let threadContext = StatusItem.Thread.Context(status: status) - super.init( - context: context, - authContext: authContext, - optionalRoot: .root(context: threadContext) - ) - } -} diff --git a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift index 08e1c7c0f..9e6018ccd 100644 --- a/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift +++ b/Mastodon/Scene/Thread/MastodonStatusThreadViewModel.swift @@ -87,7 +87,7 @@ extension MastodonStatusThreadViewModel { } let items = self.__ancestors + newItems - self.__ancestors = items + self.__ancestors = items.removingDuplicates() } func appendDescendant( @@ -122,7 +122,7 @@ extension MastodonStatusThreadViewModel { guard !items.contains(item) else { continue } items.append(item) } - self.__descendants = items + self.__descendants = items.removingDuplicates() } } diff --git a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift index 14b5f18a2..a2f83a1dd 100644 --- a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift @@ -55,12 +55,13 @@ extension ThreadViewController: DataSourceProvider { } private func handle(status: MastodonStatus) { - viewModel.mastodonStatusThreadViewModel.ancestors.handle(status: status, for: viewModel) - viewModel.mastodonStatusThreadViewModel.descendants.handle(status: status, for: viewModel) + viewModel.mastodonStatusThreadViewModel.ancestors.handleUpdate(status: status, for: viewModel) + viewModel.mastodonStatusThreadViewModel.descendants.handleUpdate(status: status, for: viewModel) } func delete(status: MastodonStatus) { - assertionFailure("Needs implementation") + viewModel.mastodonStatusThreadViewModel.ancestors.handleDelete(status: status, for: viewModel) + viewModel.mastodonStatusThreadViewModel.descendants.handleDelete(status: status, for: viewModel) } @MainActor @@ -70,7 +71,7 @@ extension ThreadViewController: DataSourceProvider { } private extension [StatusItem] { - mutating func handle(status: MastodonStatus, for viewModel: ThreadViewModel) { + mutating func handleUpdate(status: MastodonStatus, for viewModel: ThreadViewModel) { for (index, ancestor) in enumerated() { switch ancestor { case let .feed(record): @@ -105,4 +106,40 @@ private extension [StatusItem] { } } } + + mutating func handleDelete(status: MastodonStatus, for viewModel: ThreadViewModel) { + for (index, ancestor) in enumerated() { + switch ancestor { + case let .feed(record): + if record.status?.id == status.id { + self.remove(at: index) + } + case let.feedLoader(record): + if record.status?.id == status.id { + self.remove(at: index) + } + case let .status(record): + if record.id == status.id { + self.remove(at: index) + } + case let .thread(thread): + switch thread { + case let .root(context): + if context.status.id == status.id { + self.remove(at: index) + } + case let .reply(context): + if context.status.id == status.id { + self.remove(at: index) + } + case let .leaf(context): + if context.status.id == status.id { + self.remove(at: index) + } + } + case .bottomLoader, .topLoader: + break + } + } + } } diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index dff7dc047..7c9998746 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -115,8 +115,10 @@ extension ThreadViewModel.LoadThreadState { class NoMore: ThreadViewModel.LoadThreadState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return false + switch stateClass { + case is Loading.Type: return true + default: return false + } } } - } diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index b4bf03d93..c12360215 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -82,8 +82,15 @@ class ThreadViewModel { context.publisherService .statusPublishResult .sink { [weak self] value in - if case let Result.success(result) = value, case StatusPublishResult.edit = result { - self?.hasPendingStatusEditReload = true + if case let Result.success(result) = value { + switch result { + case .edit: + self?.hasPendingStatusEditReload = true + case .post: + guard let self else { return } + self.loadThreadStateMachine.enter(LoadThreadState.Loading.self) + + } } } .store(in: &disposeBag) From 9167260e3044637c155e0f2d152732f6062e0921 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 14 Dec 2023 09:56:50 +0100 Subject: [PATCH 039/159] Update datasource on delete statuses in timelines and thread (IOS-212) --- ...hreadViewController+DataSourceProvider.swift | 4 ++++ .../Scene/Thread/ThreadViewController.swift | 17 +++++++++++++++++ Mastodon/Scene/Thread/ThreadViewModel.swift | 2 ++ 3 files changed, 23 insertions(+) diff --git a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift index a2f83a1dd..f3373340b 100644 --- a/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Thread/ThreadViewController+DataSourceProvider.swift @@ -60,6 +60,10 @@ extension ThreadViewController: DataSourceProvider { } func delete(status: MastodonStatus) { + if viewModel.root?.record.id == status.id { + viewModel.root = nil + viewModel.onDismiss.send(status) + } viewModel.mastodonStatusThreadViewModel.ancestors.handleDelete(status: status, for: viewModel) viewModel.mastodonStatusThreadViewModel.descendants.handleDelete(status: status, for: viewModel) } diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index 584f60753..0dcd473a1 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -14,6 +14,7 @@ import MastodonAsset import MastodonCore import MastodonUI import MastodonLocalization +import MastodonSDK final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -70,6 +71,14 @@ extension ThreadViewController { } .store(in: &disposeBag) + viewModel.onDismiss + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] status in + self?.navigationController?.popViewController(animated: true) + self?.navigationController?.notifyChildrenAboutStatusDeletion(status) + }) + .store(in: &disposeBag) + tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) tableView.pinToParent() @@ -182,3 +191,11 @@ extension ThreadViewController: StatusTableViewControllerNavigateable { statusKeyCommandHandler(sender) } } + +extension UINavigationController { + func notifyChildrenAboutStatusDeletion(_ status: MastodonStatus) { + viewControllers.compactMap { $0 as? DataSourceProvider }.forEach { provider in + provider?.delete(status: status ) + } + } +} diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index c12360215..2a2e76a66 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -32,6 +32,8 @@ class ThreadViewModel { @Published var threadContext: ThreadContext? @Published var hasPendingStatusEditReload = false + let onDismiss = PassthroughSubject() + private(set) lazy var loadThreadStateMachine: GKStateMachine = { let stateMachine = GKStateMachine(states: [ LoadThreadState.Initial(viewModel: self), From d759a4b69a23728698bd9f0623cc0ad684a6ca91 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 14 Dec 2023 10:11:05 +0100 Subject: [PATCH 040/159] Update posts on timeline and thread if edited (IOS-211) --- .../Scene/Thread/ThreadViewController.swift | 13 +++++++++++++ Mastodon/Scene/Thread/ThreadViewModel.swift | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index 0dcd473a1..ed8808b32 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -79,6 +79,13 @@ extension ThreadViewController { }) .store(in: &disposeBag) + viewModel.onEdit + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] status in + self?.navigationController?.notifyChildrenAboutStatusUpdate(status) + }) + .store(in: &disposeBag) + tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) tableView.pinToParent() @@ -198,4 +205,10 @@ extension UINavigationController { provider?.delete(status: status ) } } + + func notifyChildrenAboutStatusUpdate(_ status: MastodonStatus) { + viewControllers.compactMap { $0 as? DataSourceProvider }.forEach { provider in + provider?.update(status: status ) + } + } } diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index 2a2e76a66..a6957049e 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -33,6 +33,7 @@ class ThreadViewModel { @Published var hasPendingStatusEditReload = false let onDismiss = PassthroughSubject() + let onEdit = PassthroughSubject() private(set) lazy var loadThreadStateMachine: GKStateMachine = { let stateMachine = GKStateMachine(states: [ @@ -84,14 +85,20 @@ class ThreadViewModel { context.publisherService .statusPublishResult .sink { [weak self] value in + guard let self else { return } if case let Result.success(result) = value { switch result { - case .edit: - self?.hasPendingStatusEditReload = true - case .post: - guard let self else { return } + case let .edit(content): + let status = content.value + let mastodonStatus = MastodonStatus.fromEntity(status) + self.hasPendingStatusEditReload = true + if status.id == root?.record.id { + self.root = .root(context: .init(status: mastodonStatus)) + } + self.loadThreadStateMachine.enter(LoadThreadState.Loading.self) + self.onEdit.send(mastodonStatus) + case .post: self.loadThreadStateMachine.enter(LoadThreadState.Loading.self) - } } } From 22324f4c1e183b750cb9e8bcabcc6b70dc2e9e2f Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 14 Dec 2023 16:33:50 +0100 Subject: [PATCH 041/159] Fix Polls not showing up (IOS-213) --- Mastodon/Diffable/Status/StatusSection.swift | 2 +- .../Content/PollOptionView+Configuration.swift | 8 ++++---- .../Service/API/APIService+HashtagTimeline.swift | 14 ++++++++++++++ .../Service/API/APIService+HomeTimeline.swift | 14 ++++++++++++++ .../Service/API/APIService+PublicTimeline.swift | 14 ++++++++++++++ .../Service/API/APIService+Status.swift | 13 +++++++++++++ 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/Mastodon/Diffable/Status/StatusSection.swift b/Mastodon/Diffable/Status/StatusSection.swift index d9c1b4725..12fce16d8 100644 --- a/Mastodon/Diffable/Status/StatusSection.swift +++ b/Mastodon/Diffable/Status/StatusSection.swift @@ -165,7 +165,7 @@ extension StatusSection { return } - cell.pollOptionView.configure(pollOption: option) + cell.pollOptionView.configure(pollOption: option, status: statusView.viewModel.originalStatus) // trigger update if needs let needsUpdatePoll: Bool = { diff --git a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift index 631e5b337..c815190be 100644 --- a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift @@ -14,8 +14,8 @@ import MastodonUI import MastodonSDK extension PollOptionView { - public func configure(pollOption option: PollOption) { - guard let poll = option.poll, let status = poll.status else { + public func configure(pollOption option: PollOption, status: MastodonStatus?) { + guard let poll = option.poll else { assertionFailure("PollOption to be configured is expected to be part of Poll with Status") return } @@ -48,8 +48,8 @@ extension PollOptionView { viewModel.isMultiple = poll.multiple let optionIndex = option.index - let authorDomain = status.author.domain - let authorID = status.author.id + let authorDomain = status?.entity.account.domain ?? "" + let authorID = status?.entity.account.id ?? "" // isSelect, isPollVoted, isMyPoll Publishers.CombineLatest4( option.publisher(for: \.poll), diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index 4edd34bf3..f1d7c0688 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -41,6 +41,20 @@ extension APIService { hashtag: hashtag, authorization: authorization ).singleOutput() + + #warning("TODO: Remove this with IOS-181, IOS-182") + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let me = authenticationBox.authentication.user(in: managedObjectContext) + + for entity in response.value { + guard let poll = entity.poll else { continue } + _ = Persistence.Poll.createOrMerge( + in: managedObjectContext, + context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) + ) + } + } return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 047fe1337..e6e3e9504 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -41,6 +41,20 @@ extension APIService { authorization: authorization ).singleOutput() + #warning("TODO: Remove this with IOS-181, IOS-182") + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let me = authenticationBox.authentication.user(in: managedObjectContext) + + for entity in response.value { + guard let poll = entity.poll else { continue } + _ = Persistence.Poll.createOrMerge( + in: managedObjectContext, + context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) + ) + } + } + // FIXME: This is a dirty hack to make the performance-stuff work. // Problem is, that we don't persist the user on disk anymore. So we have to fetch // it when we need it to display on the home timeline. diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift index 9617c5233..6fa686bf9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift @@ -26,6 +26,20 @@ extension APIService { query: query, authorization: authorization ).singleOutput() + + #warning("TODO: Remove this with IOS-181, IOS-182") + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let me = authenticationBox.authentication.user(in: managedObjectContext) + + for entity in response.value { + guard let poll = entity.poll else { continue } + _ = Persistence.Poll.createOrMerge( + in: managedObjectContext, + context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) + ) + } + } return response } // end func diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift index 22f4b0d81..a52d4506c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -26,6 +26,19 @@ extension APIService { statusID: statusID, authorization: authorization ).singleOutput() + + #warning("TODO: Remove this with IOS-181, IOS-182") + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let me = authenticationBox.authentication.user(in: managedObjectContext) + + if let poll = response.value.poll { + _ = Persistence.Poll.createOrMerge( + in: managedObjectContext, + context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) + ) + } + } return response } From 275fa53f80d0acae8ab87a54e9589f7003ae6e5e Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 27 Dec 2023 10:32:14 +0100 Subject: [PATCH 042/159] Fix DataSourceFacade+Bookmark protocol constraints (IOS-176) --- Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 5e342108b..0bc0c9099 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -13,7 +13,7 @@ import MastodonSDK extension DataSourceFacade { public static func responseToStatusBookmarkAction( - provider: UIViewController & NeedsDependency & AuthContextProvider & DataSourceProvider, + provider: NeedsDependency & AuthContextProvider & DataSourceProvider, status: MastodonStatus ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() From 82cc0f2f3fc6e2940558a01ff6e4b7699fe7992a Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 27 Dec 2023 10:35:00 +0100 Subject: [PATCH 043/159] Tie JSON cache to domain based unique identifier --- Mastodon/Coordinator/SceneCoordinator.swift | 8 +++---- .../Persistence/FileManager+Timeline.swift | 24 +++++++++---------- .../HomeTimelineViewController.swift | 8 +++---- .../HomeTimeline/HomeTimelineViewModel.swift | 4 ++-- .../NotificationTimelineViewModel.swift | 8 +++---- .../Persistence/Persistence.swift | 24 +++++++++++-------- 6 files changed, 40 insertions(+), 36 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 4e61c31ea..cdc0c3f58 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -626,10 +626,10 @@ extension SceneCoordinator: SettingsCoordinatorDelegate { try await self.appContext.authenticationService.signOutMastodonUser( authenticationBox: authContext.mastodonAuthenticationBox ) - let userId = authContext.mastodonAuthenticationBox.userID - FileManager.default.invalidateHomeTimelineCache(for: userId) - FileManager.default.invalidateNotificationsAll(for: userId) - FileManager.default.invalidateNotificationsMentions(for: userId) + let userIdentifier = authContext.mastodonAuthenticationBox + FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) + FileManager.default.invalidateNotificationsAll(for: userIdentifier) + FileManager.default.invalidateNotificationsMentions(for: userIdentifier) self.setup() } diff --git a/Mastodon/Persistence/FileManager+Timeline.swift b/Mastodon/Persistence/FileManager+Timeline.swift index e9720212c..0a7046eaa 100644 --- a/Mastodon/Persistence/FileManager+Timeline.swift +++ b/Mastodon/Persistence/FileManager+Timeline.swift @@ -8,15 +8,15 @@ extension FileManager { private static let cacheItemsLimit: Int = 100 // max number of items to cache // Retrieve - func cachedHomeTimeline(for userId: String) throws -> [MastodonStatus] { + func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] { try cached(timeline: .homeTimeline(userId)).map(MastodonStatus.fromEntity) } - func cachedNotificationsAll(for userId: String) throws -> [Mastodon.Entity.Notification] { + func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { try cached(timeline: .notificationsAll(userId)) } - func cachedNotificationsMentions(for userId: String) throws -> [Mastodon.Entity.Notification] { + func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { try cached(timeline: .notificationsMentions(userId)) } @@ -38,16 +38,16 @@ extension FileManager { } // Create - func cacheHomeTimeline(items: [MastodonStatus], for userId: String) { - cache(items.map { $0.entity }, timeline: .homeTimeline(userId)) + func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) { + cache(items.map { $0.entity }, timeline: .homeTimeline(userIdentifier)) } - func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userId: String) { - cache(items, timeline: .notificationsAll(userId)) + func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { + cache(items, timeline: .notificationsAll(userIdentifier)) } - func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userId: String) { - cache(items, timeline: .notificationsMentions(userId)) + func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { + cache(items, timeline: .notificationsMentions(userIdentifier)) } private func cache(_ items: [T], timeline: Persistence) { @@ -71,15 +71,15 @@ extension FileManager { } // Delete - func invalidateHomeTimelineCache(for userId: String) { + func invalidateHomeTimelineCache(for userId: UserIdentifier) { invalidate(timeline: .homeTimeline(userId)) } - func invalidateNotificationsAll(for userId: String) { + func invalidateNotificationsAll(for userId: UserIdentifier) { invalidate(timeline: .notificationsAll(userId)) } - func invalidateNotificationsMentions(for userId: String) { + func invalidateNotificationsMentions(for userId: UserIdentifier) { invalidate(timeline: .notificationsMentions(userId)) } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 54cc8a722..4d0a2613e 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -386,10 +386,10 @@ extension HomeTimelineViewController { @objc func signOutAction(_ sender: UIAction) { Task { @MainActor in try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox) - let userId = viewModel.authContext.mastodonAuthenticationBox.userID - FileManager.default.invalidateHomeTimelineCache(for: userId) - FileManager.default.invalidateNotificationsAll(for: userId) - FileManager.default.invalidateNotificationsMentions(for: userId) + let userIdentifier = viewModel.authContext.mastodonAuthenticationBox + FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) + FileManager.default.invalidateNotificationsAll(for: userIdentifier) + FileManager.default.invalidateNotificationsMentions(for: userIdentifier) self.coordinator.setup() } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 670103c5e..70aafb730 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -84,7 +84,7 @@ final class HomeTimelineViewModel: NSObject { self.fetchedResultsController = FeedFetchedResultsController(context: context, authContext: authContext) self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) super.init() - self.fetchedResultsController.records = (try? FileManager.default.cachedHomeTimeline(for: authContext.mastodonAuthenticationBox.userID).map { + self.fetchedResultsController.records = (try? FileManager.default.cachedHomeTimeline(for: authContext.mastodonAuthenticationBox).map { MastodonFeed.fromStatus($0, kind: .home) }) ?? [] @@ -111,7 +111,7 @@ final class HomeTimelineViewModel: NSObject { guard let status = feed.status else { return nil } return status } - FileManager.default.cacheHomeTimeline(items: items, for: authContext.mastodonAuthenticationBox.userID) + FileManager.default.cacheHomeTimeline(items: items, for: authContext.mastodonAuthenticationBox) }) .store(in: &disposeBag) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index c18d9fdd2..cdc1840fe 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -56,11 +56,11 @@ final class NotificationTimelineViewModel { switch scope { case .everything: - self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox.userID))?.map({ notification in + self.feedFetchedResultsController.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.userID))?.map({ notification in + self.feedFetchedResultsController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox))?.map({ notification in MastodonFeed.fromNotification(notification, kind: .notificationMentions) }) ?? [] } @@ -75,9 +75,9 @@ final class NotificationTimelineViewModel { } switch self.scope { case .everything: - FileManager.default.cacheNotificationsAll(items: items, for: authContext.mastodonAuthenticationBox.userID) + FileManager.default.cacheNotificationsAll(items: items, for: authContext.mastodonAuthenticationBox) case .mentions: - FileManager.default.cacheNotificationsMentions(items: items, for: authContext.mastodonAuthenticationBox.userID) + FileManager.default.cacheNotificationsMentions(items: items, for: authContext.mastodonAuthenticationBox) } }) .store(in: &disposeBag) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index 1e3090c2e..989c5c5db 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -10,20 +10,24 @@ import Foundation public enum Persistence { case searchHistory - case homeTimeline(String) - case notificationsMentions(String) - case notificationsAll(String) + case homeTimeline(UserIdentifier) + case notificationsMentions(UserIdentifier) + case notificationsAll(UserIdentifier) + + private func uniqueUserDomainIdentifier(for userIdentifier: UserIdentifier) -> String { + "\(userIdentifier.userID)@\(userIdentifier.domain)" + } private var filename: String { switch self { case .searchHistory: - return "search_history" - case let .homeTimeline(userId): - return "home_timeline_\(userId)" - case let .notificationsMentions(userId): - return "notifications_mentions_\(userId)" - case let .notificationsAll(userId): - return "notifications_all_\(userId)" + return "search_history" // todo: @zeitschlag should this be user-scoped as well? + case let .homeTimeline(userIdentifier): + return "home_timeline_\(uniqueUserDomainIdentifier(for: userIdentifier))" + case let .notificationsMentions(userIdentifier): + return "notifications_mentions_\(uniqueUserDomainIdentifier(for: userIdentifier))" + case let .notificationsAll(userIdentifier): + return "notifications_all_\(uniqueUserDomainIdentifier(for: userIdentifier))" } } From f6e5f539d6892a3a3ec77565bf85cbcdaaa2c310 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 27 Dec 2023 10:35:49 +0100 Subject: [PATCH 044/159] Remove unused code --- Mastodon/Scene/Root/MainTab/MainTabBarController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 331c84264..42faa61fc 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -261,7 +261,6 @@ extension MainTabBarController { placeholder: .placeholder(color: .systemFill), scaleToSize: MainTabBarController.avatarButtonSize ) - // self.avatarButton.avatarImageView.image = UIImage(named: "AppIcon") } .store(in: &disposeBag) From 48746157392c16f4b104431c835a1f377bb338fd Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 27 Dec 2023 10:36:48 +0100 Subject: [PATCH 045/159] Fix incorrectly named variable --- .../SearchResult/SearchResultViewModel+State.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index a550d90db..23b193bf9 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -123,7 +123,7 @@ extension SearchResultViewModel.State { // discard result when state is not Loading guard stateMachine.currentState is Loading else { return } - let statusIDs = searchResults.statuses.map { MastodonStatus.fromEntity($0) } + let statuses = searchResults.statuses.map { MastodonStatus.fromEntity($0) } let accounts = searchResults.accounts @@ -137,7 +137,7 @@ extension SearchResultViewModel.State { relationships = [] } - let isNoMore = accounts.isEmpty && statusIDs.isEmpty + let isNoMore = accounts.isEmpty && statuses.isEmpty if viewModel.searchScope == .all || isNoMore { await enter(state: NoMore.self) @@ -153,7 +153,7 @@ extension SearchResultViewModel.State { viewModel.hashtags = [] } - await viewModel.statusFetchedResultsController.appendRecords(statusIDs) + await viewModel.statusFetchedResultsController.appendRecords(statuses) var existingRelationships = viewModel.relationships From 80df919da1ffd6e263e48e625e3ab003c8040b22 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 27 Dec 2023 10:42:56 +0100 Subject: [PATCH 046/159] Remove switch case --- Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 7c9998746..807f7575d 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -115,10 +115,7 @@ extension ThreadViewModel.LoadThreadState { class NoMore: ThreadViewModel.LoadThreadState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - switch stateClass { - case is Loading.Type: return true - default: return false - } + stateClass is Loading.Type } } } From 405b175bdf6418698db9cb2d7e0c3bc391ee2681 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 27 Dec 2023 14:46:31 +0100 Subject: [PATCH 047/159] Implement alternative Author for DataSourceFacade.MenuContext (IOS-176) --- .../Provider/DataSourceFacade+Follow.swift | 9 +++++ .../Provider/DataSourceFacade+Status.swift | 3 +- ...er+NotificationTableViewCellDelegate.swift | 1 + ...Provider+StatusTableViewCellDelegate.swift | 5 ++- .../Scene/Profile/ProfileViewController.swift | 1 + .../Service/API/APIService+Follow.swift | 34 +++++++++++++++++++ 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 2fc7e6c80..e3445115d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -126,4 +126,13 @@ extension DataSourceFacade { for: user, authenticationBox: dependency.authContext.mastodonAuthenticationBox) } + + static func responseToShowHideReblogAction( + dependency: NeedsDependency & AuthContextProvider, + user: Mastodon.Entity.Account + ) async throws { + _ = try await dependency.context.apiService.toggleShowReblogs( + for: user, + authenticationBox: dependency.authContext.mastodonAuthenticationBox) + } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index df8f634c6..26e11d026 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -144,7 +144,8 @@ extension DataSourceFacade { extension DataSourceFacade { struct MenuContext { - let author: ManagedObjectRecord? + let author: ManagedObjectRecord? // todo: Remove once IOS-192 is ready + let authorEntity: Mastodon.Entity.Account? let statusViewModel: StatusView.ViewModel? let button: UIButton? let barButtonItem: UIBarButtonItem? diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 8d8e62bd9..546b5e974 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -44,6 +44,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut action: action, menuContext: .init( author: author, + authorEntity: notification.entity.account, statusViewModel: nil, button: button, barButtonItem: nil diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index c0a6c2381..d380c4f36 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -471,8 +471,10 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte assertionFailure("only works for status data provider") return } + + let status = _status.reblog ?? _status + let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - let status = _status.reblog ?? _status let request = MastodonUser.sortedFetchRequest request.predicate = MastodonUser.predicate(domain: self.authContext.mastodonAuthenticationBox.domain, id: status.entity.account.id) request.fetchLimit = 1 @@ -518,6 +520,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte action: action, menuContext: .init( author: author, + authorEntity: status.entity.account, statusViewModel: statusViewModel, button: button, barButtonItem: nil diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 8ece9ac2b..21ebedee2 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -895,6 +895,7 @@ extension ProfileViewController: MastodonMenuDelegate { action: action, menuContext: DataSourceFacade.MenuContext( author: userRecord, + authorEntity: nil, statusViewModel: nil, button: nil, barButtonItem: self.moreMenuBarButtonItem diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index e31dbedce..b3a046bad 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -195,4 +195,38 @@ extension APIService { return try result.get() } + + public func toggleShowReblogs( + for user: Mastodon.Entity.Account, + authenticationBox: MastodonAuthenticationBox + ) async throws -> Mastodon.Response.Content { + + let result: Result, Error> + + let relationship = try await Mastodon.API.Account.relationships( + session: session, + domain: authenticationBox.domain, + query: .init(ids: [user.id]), + authorization: authenticationBox.userAuthorization + ).singleOutput().value.first + + let oldShowReblogs = relationship?.showingReblogs == true + let newShowReblogs = (oldShowReblogs == false) + + do { + let response = try await Mastodon.API.Account.follow( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + followQueryType: .follow(query: .init(reblogs: newShowReblogs)), + authorization: authenticationBox.userAuthorization + ).singleOutput() + + result = .success(response) + } catch { + result = .failure(error) + } + + return try result.get() + } } From bf433e332f98c9e9d40f9ff39e98b822580a1048 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 28 Dec 2023 13:47:07 +0100 Subject: [PATCH 048/159] Address PR Feedback (IOS-176) --- Mastodon/Coordinator/SceneCoordinator.swift | 5 +++- .../Provider/DataSourceFacade+Meta.swift | 1 - .../Provider/DataSourceFacade+Profile.swift | 26 +++++++++---------- .../Provider/DataSourceFacade+URL.swift | 1 - ...er+NotificationTableViewCellDelegate.swift | 8 +++++- ...Provider+StatusTableViewCellDelegate.swift | 2 -- ...tatusTableViewControllerNavigateable.swift | 5 +--- ...taSourceProvider+UITableViewDelegate.swift | 4 +-- ...ineViewController+DataSourceProvider.swift | 15 +++++------ ...omeTimelineViewModel+LoadOldestState.swift | 5 +--- ...ineViewController+DataSourceProvider.swift | 2 +- .../NotificationTimelineViewModel.swift | 12 --------- .../ReportStatusTableViewCell+ViewModel.swift | 1 + .../MastodonCore/Model/UserIdentifier.swift | 6 +++++ .../Persistence/Persistence.swift | 10 +++---- .../API/APIService+HashtagTimeline.swift | 4 +-- .../Service/API/APIService+HomeTimeline.swift | 4 +-- .../API/APIService+PublicTimeline.swift | 4 +-- .../View/Content/StatusCardControl.swift | 9 ++++--- .../Content/StatusView+Configuration.swift | 16 +++++++----- 20 files changed, 65 insertions(+), 75 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index cdc0c3f58..f85a7c96f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -569,7 +569,10 @@ public extension SceneCoordinator { @MainActor func showLoading(on viewController: UIViewController?) { guard let viewController else { return } - + + /// Don't add HUD twice + guard MBProgressHUD.forView(viewController.view) == nil else { return } + MBProgressHUD.showAdded(to: viewController.view, animated: true) } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift index 5140639fd..3fdf08e4e 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift @@ -50,7 +50,6 @@ extension DataSourceFacade { await responseToURLAction( provider: provider, - status: status, url: url ) case .hashtag(_, let hashtag, _): diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 3f2c6fe2d..82ec945fb 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -12,19 +12,21 @@ import MastodonSDK extension DataSourceFacade { + @MainActor static func coordinateToProfileScene( provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: MastodonStatus ) async { - let acct: String = { - switch target { - case .status: - return status.reblog?.entity.account.acct ?? status.entity.account.acct - case .reblog: - return status.entity.account.acct - } - }() + let acct: String + switch target { + case .status: + acct = status.reblog?.entity.account.acct ?? status.entity.account.acct + case .reblog: + acct = status.entity.account.acct + } + + provider.coordinator.showLoading() let _redirectRecord = try? await Mastodon.API.Account.lookupAccount( session: .shared, @@ -35,6 +37,7 @@ extension DataSourceFacade { guard let redirectRecord = _redirectRecord else { assertionFailure() + provider.coordinator.hideLoading() return } await coordinateToProfileScene( @@ -110,12 +113,9 @@ extension DataSourceFacade { return } - let managedObjectContext = provider.context.managedObjectContext - let mentions = try? await managedObjectContext.perform { - return status.entity.mentions ?? [] - } + let mentions = status.entity.mentions ?? [] - guard let mention = mentions?.first(where: { $0.url == href }) else { + guard let mention = mentions.first(where: { $0.url == href }) else { _ = provider.coordinator.present( scene: .safari(url: url), from: provider, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift b/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift index 286618a2c..ff1578af9 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+URL.swift @@ -14,7 +14,6 @@ import MastodonSDK extension DataSourceFacade { static func responseToURLAction( provider: DataSourceProvider & AuthContextProvider, - status: MastodonStatus, url: URL ) async { let domain = provider.authContext.mastodonAuthenticationBox.domain diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 546b5e974..5ed4fc8b1 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -182,9 +182,15 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med let _mediaTransitionContext: NotificationMediaTransitionContext? = { guard let status = record.status?.reblog ?? record.status else { return nil } + let needsToBeToggled: Bool = { + guard let sensitive = status.entity.sensitive else { + return false + } + return status.isSensitiveToggled ? !sensitive : sensitive + }() return NotificationMediaTransitionContext( status: status, - needsToggleMediaSensitive: status.isSensitiveToggled ? !(status.entity.sensitive == true) : (status.entity.sensitive == true) + needsToggleMediaSensitive: needsToBeToggled ) }() diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index d380c4f36..57360f0c6 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -148,7 +148,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte await DataSourceFacade.responseToURLAction( provider: self, - status: status, url: url ) } @@ -173,7 +172,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte await DataSourceFacade.responseToURLAction( provider: self, - status: status, url: url ) } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift index 97081cc6f..cfc3e07f3 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -65,10 +65,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid case .status(let record): return record case .notification(let record): - guard let statusRecord = record.status else { - return nil - } - return statusRecord + return record.status default: return nil } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 41838b0e6..276500c03 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -40,9 +40,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid provider: self, tag: tag ) - case .notification(let notification): - let managedObjectContext = context.managedObjectContext - + case .notification(let notification): let _status: MastodonStatus? = notification.status if let status = _status { await DataSourceFacade.coordinateToStatusThreadScene( diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift index 28b6a4984..aa4252b46 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift @@ -22,15 +22,12 @@ extension HomeTimelineViewController: DataSourceProvider { switch item { case .feed(let feed): - let item: DataSourceItem? = { - guard feed.kind == .home else { return nil } - if let status = feed.status { - return .status(record: status) - } else { - return nil - } - }() - return item + guard feed.kind == .home else { return nil } + if let status = feed.status { + return .status(record: status) + } else { + return nil + } default: return nil } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index a2bf3e224..5f306ea20 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -52,10 +52,7 @@ extension HomeTimelineViewModel.LoadOldestState { } Task { - let _maxID: Mastodon.Entity.Status.ID? = { - guard let status = lastFeedRecord.status else { return nil } - return status.id - }() + let _maxID = lastFeedRecord.status?.id guard let maxID = _maxID else { await self.enter(state: Fail.self) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index 4a7d22229..e7a8de3b6 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -42,7 +42,7 @@ extension NotificationTimelineViewController: DataSourceProvider { } func delete(status: MastodonStatus) { - viewModel.feedFetchedResultsController + viewModel.feedFetchedResultsController.delete(status: status) } @MainActor diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index cdc1840fe..9b552857c 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -111,18 +111,6 @@ extension NotificationTimelineViewModel { // load timeline gap func loadMore(item: NotificationItem) async { -// guard case let .feedLoader(record) = item else { return } - -// guard let maxID = record.notification?.id else { return } - -// // fetch data -// if let notifications = try? await context.apiService.notifications( -// maxID: maxID, -// scope: scope, -// authenticationBox: authContext.mastodonAuthenticationBox -// ) { -// self.feedFetchedResultsController.records += notifications.value.map { MastodonFeed.fromNotification($0, kind: record.kind) } -// } switch scope { case .everything: feedFetchedResultsController.loadNext(kind: .notificationAll) diff --git a/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift index a5ad90bc4..f5078847a 100644 --- a/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift @@ -9,6 +9,7 @@ import UIKit import MastodonSDK extension ReportStatusTableViewCell { + // todo: refactor / remove this final class ViewModel { let value: MastodonStatus diff --git a/MastodonSDK/Sources/MastodonCore/Model/UserIdentifier.swift b/MastodonSDK/Sources/MastodonCore/Model/UserIdentifier.swift index 6db7499c6..a02593f66 100644 --- a/MastodonSDK/Sources/MastodonCore/Model/UserIdentifier.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/UserIdentifier.swift @@ -13,6 +13,12 @@ public protocol UserIdentifier { var userID: Mastodon.Entity.Account.ID { get } } +public extension UserIdentifier { + var uniqueUserDomainIdentifier: String { + "\(userID)@\(domain)" + } +} + public struct MastodonUserIdentifier: UserIdentifier { public let domain: String public var userID: Mastodon.Entity.Account.ID diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index 989c5c5db..f11ff61e1 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -14,20 +14,16 @@ public enum Persistence { case notificationsMentions(UserIdentifier) case notificationsAll(UserIdentifier) - private func uniqueUserDomainIdentifier(for userIdentifier: UserIdentifier) -> String { - "\(userIdentifier.userID)@\(userIdentifier.domain)" - } - private var filename: String { switch self { case .searchHistory: return "search_history" // todo: @zeitschlag should this be user-scoped as well? case let .homeTimeline(userIdentifier): - return "home_timeline_\(uniqueUserDomainIdentifier(for: userIdentifier))" + return "home_timeline_\(userIdentifier.uniqueUserDomainIdentifier)" case let .notificationsMentions(userIdentifier): - return "notifications_mentions_\(uniqueUserDomainIdentifier(for: userIdentifier))" + return "notifications_mentions_\(userIdentifier.uniqueUserDomainIdentifier)" case let .notificationsAll(userIdentifier): - return "notifications_all_\(uniqueUserDomainIdentifier(for: userIdentifier))" + return "notifications_all_\(userIdentifier.uniqueUserDomainIdentifier)" } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index f1d7c0688..26ef9625f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -50,8 +50,8 @@ extension APIService { for entity in response.value { guard let poll = entity.poll else { continue } _ = Persistence.Poll.createOrMerge( - in: managedObjectContext, - context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) + in: managedObjectContext, + context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) ) } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index e6e3e9504..272d81fa2 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -49,8 +49,8 @@ extension APIService { for entity in response.value { guard let poll = entity.poll else { continue } _ = Persistence.Poll.createOrMerge( - in: managedObjectContext, - context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) + in: managedObjectContext, + context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) ) } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift index 6fa686bf9..ed36a57bc 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift @@ -35,8 +35,8 @@ extension APIService { for entity in response.value { guard let poll = entity.poll else { continue } _ = Persistence.Poll.createOrMerge( - in: managedObjectContext, - context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) + in: managedObjectContext, + context: .init(domain: domain, entity: poll, me: me, networkDate: response.networkDate) ) } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 14a87c6ea..c096428a5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -344,9 +344,12 @@ private extension Mastodon.Entity.Card { if !aspectRatio.isFinite { aspectRatio = 1 } - return (abs(aspectRatio - 1) < 0.05 || image == nil) && html == nil - ? .compact - : .large(aspectRatio: aspectRatio) + + if (abs(aspectRatio - 1) < 0.05 || image == nil) && html == nil { + return .compact + } else { + return .large(aspectRatio: aspectRatio) + } } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 24cf83f70..1818becaf 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -193,7 +193,6 @@ extension StatusView { } .store(in: &disposeBag) } // end if let -// } // end else B2. } // end else B. } else { @@ -389,14 +388,17 @@ extension StatusView { private func configurePoll(status: MastodonStatus) { let status = status.reblog ?? status - - let predicate = Poll.predicate(domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? "", id: status.entity.poll?.id ?? "") - + guard let context = viewModel.context?.managedObjectContext, - let poll = Poll.findOrFetch(in: context, matching: predicate) - else { return } - + let domain = viewModel.authContext?.mastodonAuthenticationBox.domain, + let pollId = status.entity.poll?.id + else { + return + } + + let predicate = Poll.predicate(domain: domain, id: pollId) + guard let poll = Poll.findOrFetch(in: context, matching: predicate) else { return } viewModel.managedObjects.insert(poll) From 682fe27319fd145a74a7b57afa1aa14eb7b4198c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 23 Nov 2023 10:26:32 +0100 Subject: [PATCH 049/159] Sprinkle in some deprecations to see what needs to be done with MastodonUser (IOS-192, IOS-189) --- Mastodon/Scene/Profile/ProfileViewModel.swift | 4 ++++ .../Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift | 1 + .../Sources/MastodonUI/ViewModel/RelationshipViewModel.swift | 1 + 3 files changed, 6 insertions(+) diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 8ed3cf03d..4be56a10e 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -33,7 +33,11 @@ class ProfileViewModel: NSObject { // input let context: AppContext let authContext: AuthContext + + @available(*, deprecated, message: "Replace with Account") @Published var me: MastodonUser? + + @available(*, deprecated, message: "Replace with Account") @Published var user: MastodonUser? let viewDidAppear = PassthroughSubject() diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 31ed535a9..8e0ef8a47 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -9,6 +9,7 @@ import CoreData import Foundation /// See also `CoreDataStack.MastodonUser`, this extension contains several +@available(*, deprecated, message: "Replace with Mastodon.Entity.Account") final public class MastodonUser: NSManagedObject { public typealias ID = String diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index 20f720d20..f24e1b47f 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -96,6 +96,7 @@ public struct RelationshipActionOptionSet: OptionSet { } } +@available(*, deprecated, message: "Replace with Mastodon.Entity.Relationship") public final class RelationshipViewModel { var disposeBag = Set() From a9fc62eda401bf6cababddba41514018a77d98a1 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 7 Dec 2023 12:37:07 +0100 Subject: [PATCH 050/159] Fix Documentation-URL --- .../Sources/MastodonSDK/Entity/Mastodon+Entity+Mention.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Mention.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Mention.swift index 182cc0138..2cfcd9d7c 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Mention.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Mention.swift @@ -15,7 +15,7 @@ extension Mastodon.Entity { /// # Last Update /// 2021/1/28 /// # Reference - /// [Document](https://docs.joinmastodon.org/entities/mention/) + /// [Document](https://docs.joinmastodon.org/entities/Status/#Mention) public struct Mention: Codable, Sendable { public typealias ID = String From 2be8d5b8dfb3d9fd72b4ac228e23234e854693c9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 7 Dec 2023 13:35:33 +0100 Subject: [PATCH 051/159] WIP: Comment out and replace user with status (IOS-192) --- .../Provider/DataSourceFacade+Follow.swift | 9 +- .../Provider/DataSourceFacade+Mute.swift | 8 +- .../Provider/DataSourceFacade+Profile.swift | 147 +++--- .../Provider/DataSourceFacade+Status.swift | 56 +-- ...er+NotificationTableViewCellDelegate.swift | 52 +-- ...Provider+StatusTableViewCellDelegate.swift | 77 ++-- ...taSourceProvider+UITableViewDelegate.swift | 54 +-- .../Provider/DataSourceProvider.swift | 1 + .../NotificationTimelineViewController.swift | 30 +- .../Profile/About/ProfileAboutViewModel.swift | 35 +- .../Header/ProfileHeaderViewController.swift | 128 +++--- .../Header/ProfileHeaderViewModel.swift | 4 +- .../ProfileHeaderView+Configuration.swift | 81 ++-- .../View/ProfileHeaderView+ViewModel.swift | 108 ++--- .../Header/View/ProfileHeaderView.swift | 24 - .../Scene/Profile/MeProfileViewModel.swift | 25 +- .../Scene/Profile/ProfileViewController.swift | 426 ++++++++---------- Mastodon/Scene/Profile/ProfileViewModel.swift | 160 +++---- .../Profile/RemoteProfileViewModel.swift | 59 +-- .../Report/Report/ReportViewController.swift | 4 +- .../Scene/Report/Report/ReportViewModel.swift | 109 ++--- .../ReportResultViewController.swift | 6 +- .../ReportResult/ReportResultViewModel.swift | 16 +- .../ReportStatusViewModel+State.swift | 12 +- .../ReportStatus/ReportStatusViewModel.swift | 6 +- .../ReportSupplementaryViewModel.swift | 6 +- ...ultViewController+DataSourceProvider.swift | 5 +- .../Scene/Settings/SettingsCoordinator.swift | 11 +- .../MastodonCore/MastodonAuthentication.swift | 8 +- .../Service/API/APIService+Account.swift | 32 +- .../Service/API/APIService+Follow.swift | 76 +--- .../Service/API/APIService+Mute.swift | 69 +-- 32 files changed, 732 insertions(+), 1112 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index e3445115d..c0b4cca77 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -120,11 +120,12 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToShowHideReblogAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + account: Mastodon.Entity.Account ) async throws { - _ = try await dependency.context.apiService.toggleShowReblogs( - for: user, - authenticationBox: dependency.authContext.mastodonAuthenticationBox) + #warning("TODO: Implement") +// _ = try await dependency.context.apiService.toggleShowReblogs( +// for: user, +// authenticationBox: dependency.authContext.mastodonAuthenticationBox) } static func responseToShowHideReblogAction( diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 1db94bd4f..4e37bc7a5 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -6,20 +6,20 @@ // import UIKit -import CoreDataStack +import MastodonSDK import MastodonCore extension DataSourceFacade { static func responseToUserMuteAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + account: Mastodon.Entity.Account ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() _ = try await dependency.context.apiService.toggleMute( - user: user, - authenticationBox: dependency.authContext.mastodonAuthenticationBox + authenticationBox: dependency.authContext.mastodonAuthenticationBox, + account: account ) } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 82ec945fb..464ecd374 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -45,21 +45,67 @@ extension DataSourceFacade { account: redirectRecord ) } - + @MainActor static func coordinateToProfileScene( provider: ViewControllerWithDependencies & AuthContextProvider, - user: ManagedObjectRecord + username: String, + domain: String ) async { - guard let user = user.object(in: provider.context.managedObjectContext) else { - assertionFailure() - return + provider.coordinator.showLoading() + + Task { + do { + guard let account = try await provider.context.apiService.fetchUser(username: username, + domain: domain, + authenticationBox: provider.authContext.mastodonAuthenticationBox) else { + return provider.coordinator.hideLoading() + } + + provider.coordinator.hideLoading() + + await coordinateToProfileScene(provider: provider, account: account) + } catch { + provider.coordinator.hideLoading() + } } - + } + + @MainActor + static func coordinateToProfileScene( + provider: ViewControllerWithDependencies & AuthContextProvider, + domain: String, + accountID: String + ) async { + provider.coordinator.showLoading() + + Task { + do { + let account = try await provider.context.apiService.accountInfo( + domain: domain, + userID: accountID, + authorization: provider.authContext.mastodonAuthenticationBox.userAuthorization + ).value + + provider.coordinator.hideLoading() + + await coordinateToProfileScene(provider: provider, account: account) + } catch { + provider.coordinator.hideLoading() + } + } + } + + @MainActor + static func coordinateToProfileScene( + provider: ViewControllerWithDependencies & AuthContextProvider, + account: Mastodon.Entity.Account + ) { + let profileViewModel = ProfileViewModel( context: provider.context, authContext: provider.authContext, - optionalMastodonUser: user + account: account ) _ = provider.coordinator.present( @@ -68,31 +114,6 @@ extension DataSourceFacade { transition: .show ) } - - @MainActor - static func coordinateToProfileScene( - provider: ViewControllerWithDependencies & AuthContextProvider, - account: Mastodon.Entity.Account - ) async { - provider.coordinator.showLoading() - - guard let domain = account.domain else { return provider.coordinator.hideLoading() } - - Task { - do { - let user = try await provider.context.apiService.fetchUser(username: account.username, - domain: domain, - authenticationBox: provider.authContext.mastodonAuthenticationBox) - provider.coordinator.hideLoading() - - if let user { - await coordinateToProfileScene(provider: provider, user: user.asRecord) - } - } catch { - provider.coordinator.hideLoading() - } - } - } } extension DataSourceFacade { @@ -112,42 +133,31 @@ extension DataSourceFacade { else { return } - let mentions = status.entity.mentions ?? [] - + guard let mention = mentions.first(where: { $0.url == href }) else { - _ = provider.coordinator.present( + _ = provider.coordinator.present( scene: .safari(url: url), from: provider, transition: .safariPresent(animated: true, completion: nil) ) + return } - let userID = mention.id - let profileViewModel: ProfileViewModel = { - // check if self - guard userID != provider.authContext.mastodonAuthenticationBox.userID else { - return MeProfileViewModel(context: provider.context, authContext: provider.authContext) - } - - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: userID) - let _user = provider.context.managedObjectContext.safeFetch(request).first - - if let user = _user { - return ProfileViewModel(context: provider.context, authContext: provider.authContext, optionalMastodonUser: user) - } else { - return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID) - } - }() - - _ = provider.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: provider, - transition: .show - ) +#warning("TODO: Implement") + await DataSourceFacade.coordinateToProfileScene(provider: provider, domain: "", accountID: mention.id) +// let profileViewModel = ProfileViewModel( +// context: provider.context, +// authContext: provider.authContext, +// account: status.entity.account +// ) +// +// _ = provider.coordinator.present( +// scene: .profile(viewModel: profileViewModel), +// from: provider, +// transition: .show +// ) } } @@ -166,20 +176,11 @@ extension DataSourceFacade { static func createActivityViewController( dependency: NeedsDependency, - user: ManagedObjectRecord - ) async throws -> UIActivityViewController? { - let managedObjectContext = dependency.context.managedObjectContext - let activityItems: [Any] = try await managedObjectContext.perform { - guard let user = user.object(in: managedObjectContext) else { return [] } - return user.activityItems - } - guard !activityItems.isEmpty else { - assertionFailure() - return nil - } - - let activityViewController = await UIActivityViewController( - activityItems: activityItems, + account: Mastodon.Entity.Account + ) -> UIActivityViewController { + + let activityViewController = UIActivityViewController( + activityItems: [account.url], applicationActivities: [SafariActivity(sceneCoordinator: dependency.coordinator)] ) return activityViewController diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 26e11d026..375c48575 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -144,8 +144,7 @@ extension DataSourceFacade { extension DataSourceFacade { struct MenuContext { - let author: ManagedObjectRecord? // todo: Remove once IOS-192 is ready - let authorEntity: Mastodon.Entity.Account? + let author: Mastodon.Entity.Account let statusViewModel: StatusView.ViewModel? let button: UIButton? let barButtonItem: UIBarButtonItem? @@ -176,17 +175,9 @@ extension DataSourceFacade { guard let dependency else { return } Task { - let managedObjectContext = dependency.context.managedObjectContext - let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } - return ManagedObjectRecord(objectID: user.objectID) - } - - guard let user = _user else { return } - try await DataSourceFacade.responseToShowHideReblogAction( dependency: dependency, - user: user + account: menuContext.author ) } } @@ -207,17 +198,11 @@ extension DataSourceFacade { title: actionContext.isMuting ? L10n.Common.Controls.Friendship.unmute : L10n.Common.Controls.Friendship.mute, style: .destructive ) { [weak dependency] _ in - guard let dependency = dependency else { return } + guard let dependency else { return } Task { - let managedObjectContext = dependency.context.managedObjectContext - let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } - return ManagedObjectRecord(objectID: user.objectID) - } - guard let user = _user else { return } try await DataSourceFacade.responseToUserMuteAction( dependency: dependency, - user: user + account: menuContext.author ) } // end Task } @@ -235,19 +220,13 @@ extension DataSourceFacade { title: actionContext.isBlocking ? L10n.Common.Controls.Friendship.unblock : L10n.Common.Controls.Friendship.block, style: .destructive ) { [weak dependency] _ in - guard let dependency = dependency else { return } + guard let dependency else { return } Task { - let managedObjectContext = dependency.context.managedObjectContext - let _user: ManagedObjectRecord? = try? await managedObjectContext.perform { - guard let user = menuContext.author?.object(in: managedObjectContext) else { return nil } - return ManagedObjectRecord(objectID: user.objectID) - } - guard let user = _user else { return } try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, - user: user + user: menuContext.author ) - } // end Task + } } alertController.addAction(confirmAction) let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) @@ -255,12 +234,11 @@ extension DataSourceFacade { dependency.present(alertController, animated: true) case .reportUser: Task { - guard let user = menuContext.author else { return } - + let reportViewModel = ReportViewModel( context: dependency.context, authContext: dependency.authContext, - user: user, + account: menuContext.author, status: menuContext.statusViewModel?.originalStatus ) @@ -272,15 +250,11 @@ extension DataSourceFacade { } // end Task case .shareUser: - guard let user = menuContext.author else { - assertionFailure() - return - } - let _activityViewController = try await DataSourceFacade.createActivityViewController( + let activityViewController = DataSourceFacade.createActivityViewController( dependency: dependency, - user: user + account: menuContext.author ) - guard let activityViewController = _activityViewController else { return } + _ = dependency.coordinator.present( scene: .activityViewController( activityViewController: activityViewController, @@ -303,7 +277,6 @@ extension DataSourceFacade { } // end Task case .shareStatus: Task { - let managedObjectContext = dependency.context.managedObjectContext guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else { assertionFailure() return @@ -380,11 +353,8 @@ extension DataSourceFacade { // do nothing, as the translation is reverted in `StatusTableViewCellDelegate` in `DataSourceProvider+StatusTableViewCellDelegate.swift`. break case .followUser(_): - - guard let author = menuContext.author else { return } - try await DataSourceFacade.responseToUserFollowAction(dependency: dependency, - user: author) + user: menuContext.author) } } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 5ed4fc8b1..e1d01a357 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -31,20 +31,11 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } - let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - return .init(objectID: notification.account.objectID) - } - guard let author = _author else { - assertionFailure() - return - } - try await DataSourceFacade.responseToMenuAction( dependency: self, action: action, menuContext: .init( - author: author, - authorEntity: notification.entity.account, + author: notification.entity.account, statusViewModel: nil, button: button, barButtonItem: nil @@ -71,16 +62,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for status data provider") return } - let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - return .init(objectID: notification.account.objectID) - } - guard let author = _author else { - assertionFailure() - return - } + await DataSourceFacade.coordinateToProfileScene( provider: self, - user: author + account: notification.entity.account ) } // end Task } @@ -322,7 +307,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut await DataSourceFacade.coordinateToProfileScene( provider: self, - user: notification.account.asRecord + account: notification.entity.account ) } // end Task } @@ -494,21 +479,20 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } switch item { - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) - case .notification: - assertionFailure("TODO") - default: - assertionFailure("TODO") + case .status(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + case .user(let user): + break + case .account(let account, let relationship): + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + case .notification: + assertionFailure("TODO") + case .hashtag(_): + assertionFailure("TODO") } } // end Task } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 57360f0c6..115f077c4 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -35,26 +35,14 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte } switch await statusView.viewModel.header { - case .none: - break - case .reply: - let _replyToAuthor: ManagedObjectRecord? = try? await context.managedObjectContext.perform { - guard let inReplyToAccountID = status.entity.inReplyToAccountID else { return nil } - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: domain, id: inReplyToAccountID) - request.fetchLimit = 1 - guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } - return .init(objectID: author.objectID) - } - guard let replyToAuthor = _replyToAuthor else { - assertionFailure() - return - } - - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: replyToAuthor - ) + case .none: + break + case .reply: + guard let replyToAccountID = status.entity.inReplyToAccountID else { return } + #warning("TODO: Implement Domain") + await DataSourceFacade.coordinateToProfileScene(provider: self, + domain: "", + accountID: replyToAccountID) case .repost: await DataSourceFacade.coordinateToProfileScene( @@ -469,21 +457,9 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte assertionFailure("only works for status data provider") return } - + let status = _status.reblog ?? _status - let _author: ManagedObjectRecord? = try await self.context.managedObjectContext.perform { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: self.authContext.mastodonAuthenticationBox.domain, id: status.entity.account.id) - request.fetchLimit = 1 - guard let author = self.context.managedObjectContext.safeFetch(request).first else { return nil } - return .init(objectID: author.objectID) - } - guard let author = _author else { - assertionFailure() - return - } - if case .translateStatus = action { DispatchQueue.main.async { if let cell = cell as? StatusTableViewCell { @@ -517,8 +493,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte dependency: self, action: action, menuContext: .init( - author: author, - authorEntity: status.entity.account, + author: status.entity.account, statusViewModel: statusViewModel, button: button, barButtonItem: nil @@ -709,21 +684,23 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte return } switch item { - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) - case .notification: - assertionFailure("TODO") - default: - assertionFailure("TODO") + case .status(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + case .account(let account, _): + await DataSourceFacade.coordinateToProfileScene( + provider: self, + account: account + ) + case .user(_): + assertionFailure("TODO") + case .notification: + assertionFailure("TODO") + case .hashtag(_): + assertionFailure("TODO") } } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 276500c03..bf3570dbc 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -24,43 +24,37 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid switch item { case .account(let account, relationship: _): await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) - case .hashtag(let tag): - await DataSourceFacade.coordinateToHashtagScene( - provider: self, - tag: tag - ) - case .notification(let notification): - let _status: MastodonStatus? = notification.status - if let status = _status { + case .status(let status): await DataSourceFacade.coordinateToStatusThreadScene( provider: self, - target: .status, // remove reblog wrapper + target: .status, // remove reblog wrapper status: status ) - } else { - let _author: ManagedObjectRecord? = notification.account.asRecord - if let author = _author { + case .user(let user): + break + case .hashtag(let tag): + await DataSourceFacade.coordinateToHashtagScene( + provider: self, + 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 + status: status + ) + } else { await DataSourceFacade.coordinateToProfileScene( provider: self, - user: author - ) + account: notification.entity.account) } - } - } - } // end Task - } // end func - + } // end Task + } // end func + } } extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableViewController { diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index d3b18240e..30adaeacb 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -13,6 +13,7 @@ import class CoreDataStack.Notification enum DataSourceItem: Hashable { case status(record: MastodonStatus) + @available(*, deprecated, message: "Use .account") case user(record: ManagedObjectRecord) case hashtag(tag: Mastodon.Entity.Tag) case notification(record: MastodonNotification) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 053ce9e76..0ebd0511b 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -295,25 +295,17 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { transition: .show ) } else { - context.managedObjectContext.perform { - let mastodonUserRequest = MastodonUser.sortedFetchRequest - mastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: notification.account.id) - mastodonUserRequest.fetchLimit = 1 - guard let mastodonUser = try? self.context.managedObjectContext.fetch(mastodonUserRequest).first else { - return - } - - let profileViewModel = ProfileViewModel( - context: self.context, - authContext: self.viewModel.authContext, - optionalMastodonUser: mastodonUser - ) - _ = self.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: self, - transition: .show - ) - } + + let profileViewModel = ProfileViewModel( + context: self.context, + authContext: self.viewModel.authContext, + account: notification.account + ) + _ = self.coordinator.present( + scene: .profile(viewModel: profileViewModel), + from: self, + transition: .show + ) } default: break diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift index f9a0b1c9d..5033dd60c 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift @@ -19,7 +19,7 @@ final class ProfileAboutViewModel { // input let context: AppContext - @Published var user: MastodonUser? + @Published var account: Mastodon.Entity.Account? @Published var isEditing = false @Published var accountForEdit: Mastodon.Entity.Account? @@ -34,23 +34,22 @@ final class ProfileAboutViewModel { init(context: AppContext) { self.context = context - // end init - - $user - .compactMap { $0 } - .flatMap { $0.publisher(for: \.emojis) } - .map { $0.asDictionary } - .assign(to: &$emojiMeta) - - $user - .compactMap { $0 } - .flatMap { $0.publisher(for: \.fields) } - .assign(to: &$fields) - - $user - .compactMap { $0 } - .flatMap { $0.publisher(for: \.createdAt) } - .assign(to: &$createdAt) +#warning("TODO: Implement") +// $account +// .compactMap { $0 } +// .flatMap { $0.publisher(for: \.emojis) } +// .map { $0.asDictionary } +// .assign(to: &$emojiMeta) +// +// $account +// .compactMap { $0 } +// .flatMap { $0.publisher(for: \.fields) } +// .assign(to: &$fields) +// +// $account +// .compactMap { $0 } +// .flatMap { $0.publisher(for: \.createdAt) } +// .assign(to: &$createdAt) Publishers.CombineLatest( $fields, diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index bc3c1dfa8..aba92c9c8 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -128,17 +128,17 @@ extension ProfileHeaderViewController { self.titleView.subtitleLabel.alpha = isTitleViewContentOffsetDidSet ? 1 : 0 } .store(in: &disposeBag) - viewModel.$user + viewModel.$account .receive(on: DispatchQueue.main) - .sink { [weak self] user in - guard let self = self else { return } - guard let user = user else { return } + .sink { [weak self] account in + guard let self, let account else { return } + self.profileHeaderView.prepareForReuse() - self.profileHeaderView.configuration(user: user) + self.profileHeaderView.configuration(account: account) } .store(in: &disposeBag) - viewModel.$relationshipActionOptionSet - .assign(to: \.relationshipActionOptionSet, on: profileHeaderView.viewModel) + viewModel.$relationship + .assign(to: \.relationship, on: profileHeaderView.viewModel) .store(in: &disposeBag) viewModel.$isMyself .assign(to: \.isMyself, on: profileHeaderView.viewModel) @@ -269,35 +269,37 @@ extension ProfileHeaderViewController { // MARK: - ProfileHeaderViewDelegate extension ProfileHeaderViewController: ProfileHeaderViewDelegate { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) { - guard let user = viewModel.user else { return } - let record: ManagedObjectRecord = .init(objectID: user.objectID) - - Task { - try await DataSourceFacade.coordinateToMediaPreviewScene( - dependency: self, - user: record, - previewContext: DataSourceFacade.ImagePreviewContext( - imageView: button.avatarImageView, - containerView: .profileAvatar(profileHeaderView) - ) - ) - } // end Task +#warning("TODO: Implement") +// guard let user = viewModel.user else { return } +// let record: ManagedObjectRecord = .init(objectID: user.objectID) +// +// Task { +// try await DataSourceFacade.coordinateToMediaPreviewScene( +// dependency: self, +// user: record, +// previewContext: DataSourceFacade.ImagePreviewContext( +// imageView: button.avatarImageView, +// containerView: .profileAvatar(profileHeaderView) +// ) +// ) +// } // end Task } func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) { - guard let user = viewModel.user else { return } - let record: ManagedObjectRecord = .init(objectID: user.objectID) - - Task { - try await DataSourceFacade.coordinateToMediaPreviewScene( - dependency: self, - user: record, - previewContext: DataSourceFacade.ImagePreviewContext( - imageView: imageView, - containerView: .profileBanner(profileHeaderView) - ) - ) - } // end Task +#warning("TODO: Implement") +// guard let account = viewModel.account else { return } +// let record: ManagedObjectRecord = .init(objectID: user.objectID) +// +// Task { +// try await DataSourceFacade.coordinateToMediaPreviewScene( +// dependency: self, +// user: record, +// previewContext: DataSourceFacade.ImagePreviewContext( +// imageView: imageView, +// containerView: .profileBanner(profileHeaderView) +// ) +// ) +// } // end Task } func profileHeaderView( @@ -331,35 +333,39 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate { // do nothing break case .follower: - guard let domain = viewModel.user?.domain, - let userID = viewModel.user?.id - else { return } - let followerListViewModel = FollowerListViewModel( - context: context, - authContext: viewModel.authContext, - domain: domain, - userID: userID - ) - _ = coordinator.present( - scene: .follower(viewModel: followerListViewModel), - from: self, - transition: .show - ) +#warning("TODO: Implement") +// guard let domain = viewModel.account.domain, +// let userID = viewModel.account.id +// else { return } +// let followerListViewModel = FollowerListViewModel( +// context: context, +// authContext: viewModel.authContext, +// domain: domain, +// userID: userID +// ) +// _ = coordinator.present( +// scene: .follower(viewModel: followerListViewModel), +// from: self, +// transition: .show +// ) + break case .following: - guard let domain = viewModel.user?.domain, - let userID = viewModel.user?.id - else { return } - let followingListViewModel = FollowingListViewModel( - context: context, - authContext: viewModel.authContext, - domain: domain, - userID: userID - ) - _ = coordinator.present( - scene: .following(viewModel: followingListViewModel), - from: self, - transition: .show - ) +#warning("TODO: Implement") +// guard let domain = viewModel.account.domain, +// let userID = viewModel.account.id +// else { return } +// let followingListViewModel = FollowingListViewModel( +// context: context, +// authContext: viewModel.authContext, +// domain: domain, +// userID: userID +// ) +// _ = coordinator.present( +// scene: .following(viewModel: followingListViewModel), +// from: self, +// transition: .show +// ) + break } } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 9301aea50..726ddf715 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -26,8 +26,8 @@ final class ProfileHeaderViewModel { let context: AppContext let authContext: AuthContext - @Published var user: MastodonUser? - @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none + @Published var account: Mastodon.Entity.Account? + @Published var relationship: Mastodon.Entity.Relationship? @Published var isMyself = false @Published var isEditing = false diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift index 8e1693142..b044b0603 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift @@ -7,49 +7,48 @@ import UIKit import Combine -import CoreDataStack +import MastodonSDK extension ProfileHeaderView { - func configuration(user: MastodonUser) { - // header - user.publisher(for: \.header) - .map { _ in user.headerImageURL() } - .assign(to: \.headerImageURL, on: viewModel) - .store(in: &disposeBag) - // avatar - user.publisher(for: \.avatar) - .map { _ in user.avatarImageURL() } - .assign(to: \.avatarImageURL, on: viewModel) - .store(in: &disposeBag) - // emojiMeta - user.publisher(for: \.emojis) - .map { $0.asDictionary } - .assign(to: \.emojiMeta, on: viewModel) - .store(in: &disposeBag) - // name - user.publisher(for: \.displayName) - .map { _ in user.displayNameWithFallback } - .assign(to: \.name, on: viewModel) - .store(in: &disposeBag) - // username - viewModel.acct = user.acctWithDomain - // bio - user.publisher(for: \.note) - .assign(to: \.note, on: viewModel) - .store(in: &disposeBag) - // dashboard - user.publisher(for: \.statusesCount) - .map { Int($0) } - .assign(to: \.statusesCount, on: viewModel) - .store(in: &disposeBag) - user.publisher(for: \.followingCount) - .map { Int($0) } - .assign(to: \.followingCount, on: viewModel) - .store(in: &disposeBag) - user.publisher(for: \.followersCount) - .map { Int($0) } - .assign(to: \.followersCount, on: viewModel) - .store(in: &disposeBag) + func configuration(account: Mastodon.Entity.Account) { + #warning("TODO: Implement") +// // header +// account.header.publisher +// .assign(to: \.headerImageURL, on: viewModel) +// .store(in: &disposeBag) +// // avatar +// account.avatar.publisher +// .assign(to: \.avatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // emojiMeta +// account.emojis.publisher +// .map { $0.asDictionary } +// .assign(to: \.emojiMeta, on: viewModel) +// .store(in: &disposeBag) +// // name +// account.publisher(for: \.displayName) +// .map { _ in account.displayNameWithFallback } +// .assign(to: \.name, on: viewModel) +// .store(in: &disposeBag) +// // username +// viewModel.acct = account.acctWithDomain +// // bio +// account.publisher(for: \.note) +// .assign(to: \.note, on: viewModel) +// .store(in: &disposeBag) +// // dashboard +// account.publisher(for: \.statusesCount) +// .map { Int($0) } +// .assign(to: \.statusesCount, on: viewModel) +// .store(in: &disposeBag) +// account.publisher(for: \.followingCount) +// .map { Int($0) } +// .assign(to: \.followingCount, on: viewModel) +// .store(in: &disposeBag) +// account.publisher(for: \.followersCount) +// .map { Int($0) } +// .assign(to: \.followersCount, on: viewModel) +// .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index be0dce929..c0cb383f7 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -14,6 +14,7 @@ import MastodonCore import MastodonUI import MastodonAsset import MastodonLocalization +import MastodonSDK extension ProfileHeaderView { class ViewModel: ObservableObject { @@ -45,15 +46,16 @@ extension ProfileHeaderView { @Published var fields: [MastodonField] = [] - @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none + @Published var relationship: Mastodon.Entity.Relationship? @Published var isRelationshipActionButtonHidden = false @Published var isMyself = false init() { - $relationshipActionOptionSet - .compactMap { $0.highPriorityAction(except: []) } - .map { $0 == .none } - .assign(to: &$isRelationshipActionButtonHidden) +#warning("TODO: Implement") +// $relationshipActionOptionSet +// .compactMap { $0.highPriorityAction(except: []) } +// .map { $0 == .none } +// .assign(to: &$isRelationshipActionButtonHidden) } } } @@ -96,13 +98,14 @@ extension ProfileHeaderView.ViewModel { } .store(in: &disposeBag) // follows you - $relationshipActionOptionSet - .map { $0.contains(.followingBy) && !$0.contains(.isMyself) } - .receive(on: DispatchQueue.main) - .sink { isFollowingBy in - view.followsYouBlurEffectView.isHidden = !isFollowingBy - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// $relationshipActionOptionSet +// .map { $0.contains(.followingBy) && !$0.contains(.isMyself) } +// .receive(on: DispatchQueue.main) +// .sink { isFollowingBy in +// view.followsYouBlurEffectView.isHidden = !isFollowingBy +// } +// .store(in: &disposeBag) // avatar Publishers.CombineLatest4( $avatarImageURL, @@ -117,18 +120,19 @@ extension ProfileHeaderView.ViewModel { )) } .store(in: &disposeBag) - // blur for blocking & blockingBy - $relationshipActionOptionSet - .map { $0.contains(.blocking) || $0.contains(.blockingBy) } - .sink { needsImageOverlayBlurred in - UIView.animate(withDuration: 0.33) { - let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil - view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect - let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil - view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect - } - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// // blur for blocking & blockingBy +// $relationshipActionOptionSet +// .map { $0.contains(.blocking) || $0.contains(.blockingBy) } +// .sink { needsImageOverlayBlurred in +// UIView.animate(withDuration: 0.33) { +// let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil +// view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect +// let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil +// view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect +// } +// } +// .store(in: &disposeBag) // name Publishers.CombineLatest4( $isEditing.removeDuplicates(), @@ -182,17 +186,18 @@ extension ProfileHeaderView.ViewModel { view.bioMetaText.configure(content: metaContent) } .store(in: &disposeBag) - $relationshipActionOptionSet - .receive(on: DispatchQueue.main) - .sink { optionSet in - let isBlocking = optionSet.contains(.blocking) - let isBlockedBy = optionSet.contains(.blockingBy) - let isSuspended = optionSet.contains(.suspended) - let isNeedsHidden = isBlocking || isBlockedBy || isSuspended - - view.bioMetaText.textView.isHidden = isNeedsHidden - } - .store(in: &disposeBag) +#warning("TODO: Implement") + // $relationshipActionOptionSet +// .receive(on: DispatchQueue.main) +// .sink { optionSet in +// let isBlocking = optionSet.contains(.blocking) +// let isBlockedBy = optionSet.contains(.blockingBy) +// let isSuspended = optionSet.contains(.suspended) +// let isNeedsHidden = isBlocking || isBlockedBy || isSuspended +// +// view.bioMetaText.textView.isHidden = isNeedsHidden +// } +// .store(in: &disposeBag) // dashboard $isMyself .receive(on: DispatchQueue.main) @@ -245,22 +250,23 @@ extension ProfileHeaderView.ViewModel { $isRelationshipActionButtonHidden .assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer) .store(in: &disposeBag) - Publishers.CombineLatest3( - $relationshipActionOptionSet, - $isEditing, - $isUpdating - ) - .receive(on: DispatchQueue.main) - .sink { relationshipActionOptionSet, isEditing, isUpdating in - if relationshipActionOptionSet.contains(.edit) { - // check .edit state and set .editing when isEditing - view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit)) - view.configure(state: isEditing ? .editing : .normal) - } else { - view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet) - } - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// Publishers.CombineLatest2( +// $relationshipActionOptionSet, +// $isEditing, +// $isUpdating +// ) +// .receive(on: DispatchQueue.main) +// .sink { relationshipActionOptionSet, isEditing, isUpdating in +// if relationshipActionOptionSet.contains(.edit) { +// // check .edit state and set .editing when isEditing +// view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit)) +// view.configure(state: isEditing ? .editing : .normal) +// } else { +// view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet) +// } +// } +// .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 92845fa4c..da9dd82e6 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -542,27 +542,3 @@ extension ProfileHeaderView: ProfileStatusDashboardViewDelegate { delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, dashboardMeterViewDidPressed: dashboardMeterView, meter: meter) } } - -#if DEBUG -import SwiftUI - -struct ProfileHeaderView_Previews: PreviewProvider { - static var previews: some View { - Group { - UIViewPreview(width: 375) { - let banner = ProfileHeaderView() - banner.bannerImageView.image = UIImage(named: "lucas-ludwig") - return banner - } - .previewLayout(.fixed(width: 375, height: 800)) - UIViewPreview(width: 375) { - let banner = ProfileHeaderView() - //banner.bannerImageView.image = UIImage(named: "peter-luo") - return banner - } - .preferredColorScheme(.dark) - .previewLayout(.fixed(width: 375, height: 800)) - } - } -} -#endif diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index ecbaef01e..0d3e3f383 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -16,19 +16,12 @@ final class MeProfileViewModel: ProfileViewModel { @MainActor init(context: AppContext, authContext: AuthContext) { - let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) + let me = authContext.mastodonAuthenticationBox.authentication.account() super.init( context: context, authContext: authContext, - optionalMastodonUser: user + account: me ) - - $me - .sink { [weak self] me in - guard let self = self else { return } - self.user = me - } - .store(in: &disposeBag) } override func viewDidLoad() { @@ -37,17 +30,9 @@ final class MeProfileViewModel: ProfileViewModel { Task { do { - - _ = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value - - try await context.managedObjectContext.performChanges { - guard let me = self.authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { - assertionFailure() - return - } - - self.me = me - } + let account = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value + self.account = account + self.me = account } catch { // do nothing? } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 21ebedee2..f9b6c7ec4 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -203,7 +203,7 @@ extension ProfileViewController { .store(in: &disposeBag) Publishers.CombineLatest4 ( - viewModel.relationshipViewModel.$isSuspended, + viewModel.account.suspended.publisher, profileHeaderViewController.viewModel.$isTitleViewDisplaying, editingAndUpdatingPublisher.eraseToAnyPublisher(), barButtonItemHiddenPublisher.eraseToAnyPublisher() @@ -296,43 +296,43 @@ extension ProfileViewController { private func bindViewModel() { // header +#warning("TODO: Implement") let headerViewModel = profileHeaderViewController.viewModel! - viewModel.$user - .assign(to: \.user, on: headerViewModel) - .store(in: &disposeBag) +// viewModel.$account +// .assign(to: \.account, on: headerViewModel) +// .store(in: &disposeBag) viewModel.$isEditing .assign(to: \.isEditing, on: headerViewModel) .store(in: &disposeBag) viewModel.$isUpdating .assign(to: \.isUpdating, on: headerViewModel) .store(in: &disposeBag) - viewModel.relationshipViewModel.$isMyself - .assign(to: \.isMyself, on: headerViewModel) - .store(in: &disposeBag) - viewModel.relationshipViewModel.$optionSet - .map { $0 ?? .none } - .assign(to: \.relationshipActionOptionSet, on: headerViewModel) +// viewModel.relationshipViewModel.$isMyself +// .assign(to: \.isMyself, on: headerViewModel) +// .store(in: &disposeBag) + viewModel.$relationship + .assign(to: \.relationship, on: headerViewModel) .store(in: &disposeBag) viewModel.$accountForEdit .assign(to: \.accountForEdit, on: headerViewModel) .store(in: &disposeBag) - +#warning("TODO: Implement") // timeline - [ - viewModel.postsUserTimelineViewModel, - viewModel.repliesUserTimelineViewModel, - viewModel.mediaUserTimelineViewModel, - ].forEach { userTimelineViewModel in - viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag) - viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag) - viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag) - } +// [ +// viewModel.postsUserTimelineViewModel, +// viewModel.repliesUserTimelineViewModel, +// viewModel.mediaUserTimelineViewModel, +// ].forEach { userTimelineViewModel in +// viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag) +// viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag) +// viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag) +// } // about let aboutViewModel = viewModel.profileAboutViewModel - viewModel.$user - .assign(to: \.user, on: aboutViewModel) - .store(in: &disposeBag) +// viewModel.$account +// .assign(to: \.account, on: aboutViewModel) +// .store(in: &disposeBag) viewModel.$isEditing .assign(to: \.isEditing, on: aboutViewModel) .store(in: &disposeBag) @@ -374,7 +374,7 @@ extension ProfileViewController { } .store(in: &disposeBag) Publishers.CombineLatest( - profileHeaderViewController.viewModel.$user, + profileHeaderViewController.viewModel.$account, profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear ) .sink { [weak self] (user, _) in @@ -382,7 +382,7 @@ extension ProfileViewController { Task { _ = try await self.context.apiService.fetchUser( username: user.username, - domain: user.domainFromAcct, + domain: "user.domainFromAcct", authenticationBox: self.authContext.mastodonAuthenticationBox ) } @@ -392,26 +392,23 @@ extension ProfileViewController { private func bindMoreBarButtonItem() { Publishers.CombineLatest( - viewModel.$user, - viewModel.relationshipViewModel.$optionSet + viewModel.$account, + viewModel.$relationship ) - .asyncMap { [weak self] user, relationshipSet -> UIMenu? in - guard let self = self else { return nil } - guard let user = user else { - return nil - } + .asyncMap { [weak self] user, relationship -> UIMenu? in + guard let self, let relationship else { return nil } + let name = user.displayNameWithFallback - let _ = ManagedObjectRecord(objectID: user.objectID) var menuActions: [MastodonMenu.Action] = [ - .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)), - .blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)), + .muteUser(.init(name: name, isMuting: relationship.muting ?? false)), + .blockUser(.init(name: name, isBlocking: relationship.blocking)), .reportUser(.init(name: name)), .shareUser(.init(name: name)), ] - if let me = self.viewModel?.me, me.following.contains(user) { - let showReblogs = me.showingReblogsBy.contains(user) + if relationship.following { + let showReblogs = relationship.showingReblogs ?? false// me.showingReblogsBy.contains(user) let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs) menuActions.insert(.hideReblogs(context), at: 1) } @@ -473,26 +470,6 @@ extension ProfileViewController { .store(in: &disposeBag) } -// private func bindProfileRelationship() { -// -// Publishers.CombineLatest3( -// viewModel.isBlocking.eraseToAnyPublisher(), -// viewModel.isBlockedBy.eraseToAnyPublisher(), -// viewModel.suspended.eraseToAnyPublisher() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isBlocking, isBlockedBy, suspended in -// guard let self = self else { return } -// let isNeedSetHidden = isBlocking || isBlockedBy || suspended -// self.profileHeaderViewController.viewModel.needsSetupBottomShadow.value = !isNeedSetHidden -// self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden -// self.profileHeaderViewController.viewModel.needsFiledCollectionViewHidden.value = isNeedSetHidden -// self.profileHeaderViewController.buttonBar.isUserInteractionEnabled = !isNeedSetHidden -// self.viewModel.needsPagePinToTop.value = isNeedSetHidden -// } -// .store(in: &disposeBag) -// } // end func bindProfileRelationship - private func handleMetaPress(_ meta: Meta) { switch meta { case .url(_, _, let url, _): @@ -525,24 +502,19 @@ extension ProfileViewController { } @objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) { - guard let user = viewModel.user else { return } - let record: ManagedObjectRecord = .init(objectID: user.objectID) - Task { - let _activityViewController = try await DataSourceFacade.createActivityViewController( - dependency: self, - user: record - ) - guard let activityViewController = _activityViewController else { return } - _ = self.coordinator.present( - scene: .activityViewController( - activityViewController: activityViewController, - sourceView: nil, - barButtonItem: sender - ), - from: self, - transition: .activityViewControllerPresent(animated: true, completion: nil) - ) - } // end Task + let activityViewController = DataSourceFacade.createActivityViewController( + dependency: self, + account: viewModel.account + ) + _ = self.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: nil, + barButtonItem: sender + ), + from: self, + transition: .activityViewControllerPresent(animated: true, completion: nil) + ) } @objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) { @@ -556,8 +528,8 @@ extension ProfileViewController { } @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { - guard let mastodonUser = viewModel.user else { return } - let mention = "@" + mastodonUser.acct + + let mention = "@" + viewModel.account.acct UITextChecker.learnWord(mention) let composeViewModel = ComposeViewModel( context: context, @@ -683,34 +655,6 @@ extension ProfileViewController: TabBarPagerDataSource { } } -//// MARK: - UIScrollViewDelegate -//extension ProfileViewController: UIScrollViewDelegate { -// -// func scrollViewDidScroll(_ scrollView: UIScrollView) { -// contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y -// let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top -// if scrollView.contentOffset.y < topMaxContentOffsetY { -// self.containerScrollView.contentOffset.y = scrollView.contentOffset.y -// for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers { -// postTimelineView.scrollView?.contentOffset.y = 0 -// } -// contentOffsets.removeAll() -// } else { -// containerScrollView.contentOffset.y = topMaxContentOffsetY -// if viewModel.needsPagePinToTop.value { -// // do nothing -// } else { -// if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer { -// let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y -// customScrollViewContainerController.scrollView?.contentOffset.y = contentOffsetY -// } -// } -// -// } -// } -// -//} - // MARK: - AuthContextProvider extension ProfileViewController: AuthContextProvider { var authContext: AuthContext { viewModel.authContext } @@ -723,140 +667,140 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton ) { - let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none - +// let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none +#warning("TODO: Implement") // handle edit logic for editable profile // handle relationship logic for non-editable profile - if relationshipActionSet.contains(.edit) { - // do nothing when updating - guard !viewModel.isUpdating else { return } - - guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } - guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } - - let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited - - if isEdited { - // update profile when edited - viewModel.isUpdating = true - Task { @MainActor in - do { - // TODO: handle error - _ = try await viewModel.updateProfileInfo( - headerProfileInfo: profileHeaderViewModel.profileInfoEditing, - aboutProfileInfo: profileAboutViewModel.profileInfoEditing - ) - self.viewModel.isEditing = false - - } catch { - let alertController = UIAlertController( - for: error, - title: L10n.Common.Alerts.EditProfileFailure.title, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) - alertController.addAction(okAction) - self.present(alertController, animated: true) - } - - // finish updating - self.viewModel.isUpdating = false - } // end Task - } else { - // set `updating` then toggle `edit` state - viewModel.isUpdating = true - viewModel.fetchEditProfileInfo() - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - defer { - // finish updating - self.viewModel.isUpdating = false - } - switch completion { - case .failure(let error): - let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) - alertController.addAction(okAction) - _ = self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - case .finished: - // enter editing mode - self.viewModel.isEditing.toggle() - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - self.viewModel.accountForEdit = response.value - } - .store(in: &disposeBag) - } - } else { - guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } - switch relationshipAction { - case .none: - break - case .follow, .request, .pending, .following: - guard let user = viewModel.user else { return } - let record = ManagedObjectRecord(objectID: user.objectID) - Task { - try await DataSourceFacade.responseToUserFollowAction( - dependency: self, - user: record - ) - } - case .muting: - guard let user = viewModel.user else { return } - let name = user.displayNameWithFallback - - let alertController = UIAlertController( - title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, - message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), - preferredStyle: .alert - ) - let record = ManagedObjectRecord(objectID: user.objectID) - let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in - guard let self = self else { return } - Task { - try await DataSourceFacade.responseToUserMuteAction( - dependency: self, - user: record - ) - } - } - alertController.addAction(unmuteAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - case .blocking: - guard let user = viewModel.user else { return } - let name = user.displayNameWithFallback - - let alertController = UIAlertController( - title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, - message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), - preferredStyle: .alert - ) - let record = ManagedObjectRecord(objectID: user.objectID) - let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in - guard let self = self else { return } - Task { - try await DataSourceFacade.responseToUserBlockAction( - dependency: self, - user: record - ) - } - } - alertController.addAction(unblockAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: - break - } - } +// if relationshipActionSet.contains(.edit) { +// // do nothing when updating +// guard !viewModel.isUpdating else { return } +// +// guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } +// guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } +// +// let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited +// +// if isEdited { +// // update profile when edited +// viewModel.isUpdating = true +// Task { @MainActor in +// do { +// // TODO: handle error +// _ = try await viewModel.updateProfileInfo( +// headerProfileInfo: profileHeaderViewModel.profileInfoEditing, +// aboutProfileInfo: profileAboutViewModel.profileInfoEditing +// ) +// self.viewModel.isEditing = false +// +// } catch { +// let alertController = UIAlertController( +// for: error, +// title: L10n.Common.Alerts.EditProfileFailure.title, +// preferredStyle: .alert +// ) +// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) +// alertController.addAction(okAction) +// self.present(alertController, animated: true) +// } +// +// // finish updating +// self.viewModel.isUpdating = false +// } // end Task +// } else { +// // set `updating` then toggle `edit` state +// viewModel.isUpdating = true +// viewModel.fetchEditProfileInfo() +// .receive(on: DispatchQueue.main) +// .sink { [weak self] completion in +// guard let self = self else { return } +// defer { +// // finish updating +// self.viewModel.isUpdating = false +// } +// switch completion { +// case .failure(let error): +// let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) +// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) +// alertController.addAction(okAction) +// _ = self.coordinator.present( +// scene: .alertController(alertController: alertController), +// from: nil, +// transition: .alertController(animated: true, completion: nil) +// ) +// case .finished: +// // enter editing mode +// self.viewModel.isEditing.toggle() +// } +// } receiveValue: { [weak self] response in +// guard let self = self else { return } +// self.viewModel.accountForEdit = response.value +// } +// .store(in: &disposeBag) +// } +// } else { +// guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } +// switch relationshipAction { +// case .none: +// break +// case .follow, .request, .pending, .following: +// guard let user = viewModel.user else { return } +// let record = ManagedObjectRecord(objectID: user.objectID) +// Task { +// try await DataSourceFacade.responseToUserFollowAction( +// dependency: self, +// user: record +// ) +// } +// case .muting: +// guard let user = viewModel.user else { return } +// let name = user.displayNameWithFallback +// +// let alertController = UIAlertController( +// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, +// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), +// preferredStyle: .alert +// ) +// let record = ManagedObjectRecord(objectID: user.objectID) +// let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in +// guard let self = self else { return } +// Task { +// try await DataSourceFacade.responseToUserMuteAction( +// dependency: self, +// user: record +// ) +// } +// } +// alertController.addAction(unmuteAction) +// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) +// alertController.addAction(cancelAction) +// present(alertController, animated: true, completion: nil) +// case .blocking: +// guard let user = viewModel.user else { return } +// let name = user.displayNameWithFallback +// +// let alertController = UIAlertController( +// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, +// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), +// preferredStyle: .alert +// ) +// let record = ManagedObjectRecord(objectID: user.objectID) +// let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in +// guard let self = self else { return } +// Task { +// try await DataSourceFacade.responseToUserBlockAction( +// dependency: self, +// user: record +// ) +// } +// } +// alertController.addAction(unblockAction) +// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) +// alertController.addAction(cancelAction) +// present(alertController, animated: true, completion: nil) +// case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: +// break +// } +// } } @@ -885,23 +829,19 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { // MARK: - MastodonMenuDelegate extension ProfileViewController: MastodonMenuDelegate { func menuAction(_ action: MastodonMenu.Action) { - guard let user = viewModel.user else { return } - - let userRecord: ManagedObjectRecord = .init(objectID: user.objectID) Task { try await DataSourceFacade.responseToMenuAction( dependency: self, action: action, menuContext: DataSourceFacade.MenuContext( - author: userRecord, - authorEntity: nil, + author: viewModel.account, statusViewModel: nil, button: nil, barButtonItem: self.moreMenuBarButtonItem ) ) - } // end Task + } } } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 4be56a10e..7a075f6ea 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -34,21 +34,16 @@ class ProfileViewModel: NSObject { let context: AppContext let authContext: AuthContext - @available(*, deprecated, message: "Replace with Account") - @Published var me: MastodonUser? + @Published var me: Mastodon.Entity.Account? + @Published var account: Mastodon.Entity.Account + @Published var relationship: Mastodon.Entity.Relationship? - @available(*, deprecated, message: "Replace with Account") - @Published var user: MastodonUser? - let viewDidAppear = PassthroughSubject() @Published var isEditing = false @Published var isUpdating = false @Published var accountForEdit: Mastodon.Entity.Account? - // output - let relationshipViewModel = RelationshipViewModel() - @Published var userIdentifier: UserIdentifier? = nil @Published var isRelationshipActionButtonHidden: Bool = true @@ -61,10 +56,10 @@ class ProfileViewModel: NSObject { // let needsPagePinToTop = CurrentValueSubject(false) @MainActor - init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) { + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account?) { self.context = context self.authContext = authContext - self.user = mastodonUser + self.account = account! self.postsUserTimelineViewModel = UserTimelineViewModel( context: context, authContext: authContext, @@ -87,93 +82,86 @@ class ProfileViewModel: NSObject { super.init() // bind me - self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) - $me - .assign(to: \.me, on: relationshipViewModel) - .store(in: &disposeBag) + self.me = authContext.mastodonAuthenticationBox.authentication.account() // bind user - $user + $account .map { user -> UserIdentifier? in - guard let user = user else { return nil } - return MastodonUserIdentifier(domain: user.domain, userID: user.id) + guard let account, let domain = account.domain else { return nil } + return MastodonUserIdentifier(domain: domain, userID: account.id) } .assign(to: &$userIdentifier) - $user - .assign(to: \.user, on: relationshipViewModel) - .store(in: &disposeBag) - + // bind userIdentifier $userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier) $userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier) $userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier) // bind bar button items - relationshipViewModel.$optionSet - .sink { [weak self] optionSet in - guard let self = self else { return } - guard let optionSet = optionSet, !optionSet.contains(.none) else { - self.isReplyBarButtonItemHidden = true - self.isMoreMenuBarButtonItemHidden = true - self.isMeBarButtonItemsHidden = true - return - } - - let isMyself = optionSet.contains(.isMyself) - self.isReplyBarButtonItemHidden = isMyself - self.isMoreMenuBarButtonItemHidden = isMyself - self.isMeBarButtonItemsHidden = !isMyself - } - .store(in: &disposeBag) +#warning("TODO: Implement") +// relationshipViewModel.$optionSet +// .sink { [weak self] optionSet in +// guard let self = self else { return } +// guard let optionSet = optionSet, !optionSet.contains(.none) else { +// self.isReplyBarButtonItemHidden = true +// self.isMoreMenuBarButtonItemHidden = true +// self.isMeBarButtonItemsHidden = true +// return +// } +// +// let isMyself = optionSet.contains(.isMyself) +// self.isReplyBarButtonItemHidden = isMyself +// self.isMoreMenuBarButtonItemHidden = isMyself +// self.isMeBarButtonItemsHidden = !isMyself +// } +// .store(in: &disposeBag) // query relationship - let userRecord = $user.map { user -> ManagedObjectRecord? in - user.flatMap { ManagedObjectRecord(objectID: $0.objectID) } - } - let pendingRetryPublisher = CurrentValueSubject(1) + #warning("TODO: Implement") +// let pendingRetryPublisher = CurrentValueSubject(1) - // observe friendship - Publishers.CombineLatest( - userRecord, - pendingRetryPublisher - ) - .sink { [weak self] userRecord, _ in - guard let self = self else { return } - guard let userRecord = userRecord else { return } - Task { - do { - let response = try await self.updateRelationship( - record: userRecord, - authenticationBox: self.authContext.mastodonAuthenticationBox - ) - // there are seconds delay after request follow before requested -> following. Query again when needs - guard let relationship = response.value.first else { return } - if relationship.requested == true { - let delay = pendingRetryPublisher.value - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in - guard let _ = self else { return } - pendingRetryPublisher.value = min(2 * delay, 60) - } - } - } catch { - } - } // end Task - } - .store(in: &disposeBag) +// // observe friendship +// Publishers.CombineLatest( +// account, +// pendingRetryPublisher +// ) +// .sink { [weak self] account, _ in +// guard let self, let account else { return } +// +// Task { +// do { +// let response = try await self.updateRelationship( +// account: account, +// authenticationBox: self.authContext.mastodonAuthenticationBox +// ) +// // there are seconds delay after request follow before requested -> following. Query again when needs +// guard let relationship = response.value.first else { return } +// if relationship.requested == true { +// let delay = pendingRetryPublisher.value +// DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in +// guard let _ = self else { return } +// pendingRetryPublisher.value = min(2 * delay, 60) +// } +// } +// } catch { +// } +// } // end Task +// } +// .store(in: &disposeBag) - let isBlockingOrBlocked = Publishers.CombineLatest( - relationshipViewModel.$isBlocking, - relationshipViewModel.$isBlockingBy - ) - .map { $0 || $1 } - .share() - - Publishers.CombineLatest( - isBlockingOrBlocked, - $isEditing - ) - .map { !$0 && !$1 } - .assign(to: &$isPagingEnabled) +// let isBlockingOrBlocked = Publishers.CombineLatest( +// relationshipViewModel.$isBlocking, +// relationshipViewModel.$isBlockingBy +// ) +// .map { $0 || $1 } +// .share() +// +// Publishers.CombineLatest( +// isBlockingOrBlocked, +// $isEditing +// ) +// .map { !$0 && !$1 } +// .assign(to: &$isPagingEnabled) } @@ -183,21 +171,21 @@ class ProfileViewModel: NSObject { // fetch profile info before edit func fetchEditProfileInfo() -> AnyPublisher, Error> { - guard let me else { + guard let me, let domain = me.domain else { return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() } let mastodonAuthentication = authContext.mastodonAuthenticationBox.authentication let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) - return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization) + return context.apiService.accountVerifyCredentials(domain: domain, authorization: authorization) } private func updateRelationship( - record: ManagedObjectRecord, + account: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { let response = try await context.apiService.relationship( - records: [record], + forAccounts: [account], authenticationBox: authenticationBox ) return response diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index b0a2f9f48..2849e1fe2 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -15,7 +15,7 @@ final class RemoteProfileViewModel: ProfileViewModel { @MainActor init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) { - super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + super.init(context: context, authContext: authContext, account: nil) let domain = authContext.mastodonAuthenticationBox.domain let authorization = authContext.mastodonAuthenticationBox.userAuthorization @@ -38,62 +38,28 @@ final class RemoteProfileViewModel: ProfileViewModel { break } } receiveValue: { [weak self] response in - guard let self = self else { return } - let managedObjectContext = context.managedObjectContext - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) - guard let mastodonUser = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - self.user = mastodonUser + self?.account = response.value } .store(in: &disposeBag) } @MainActor init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) { - super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + super.init(context: context, authContext: authContext, account: nil) Task { @MainActor in let response = try await context.apiService.notification( notificationID: notificationID, authenticationBox: authContext.mastodonAuthenticationBox ) - let userID = response.value.account.id - - let _user: MastodonUser? = try await context.managedObjectContext.perform { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID) - request.fetchLimit = 1 - return context.managedObjectContext.safeFetch(request).first - } - - if let user = _user { - self.user = user - } else { - _ = try await context.apiService.accountInfo( - domain: authContext.mastodonAuthenticationBox.domain, - userID: userID, - authorization: authContext.mastodonAuthenticationBox.userAuthorization - ) - - let _user: MastodonUser? = try await context.managedObjectContext.perform { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: authContext.mastodonAuthenticationBox.domain, id: userID) - request.fetchLimit = 1 - return context.managedObjectContext.safeFetch(request).first - } - - self.user = _user - } + + self.account = response.value.account } // end Task } @MainActor init(context: AppContext, authContext: AuthContext, acct: String){ - super.init(context: context, authContext: authContext, optionalMastodonUser: nil) + super.init(context: context, authContext: authContext, account: nil) let domain = authContext.mastodonAuthenticationBox.domain let authenticationBox = authContext.mastodonAuthenticationBox @@ -116,16 +82,9 @@ final class RemoteProfileViewModel: ProfileViewModel { break } } receiveValue: { [weak self] response in - guard let self = self, let value = response.value else { return } - let managedObjectContext = context.managedObjectContext - let request = MastodonUser.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = MastodonUser.predicate(domain: domain, id: value.id) - guard let mastodonUser = managedObjectContext.safeFetch(request).first else { - assertionFailure() - return - } - self.user = mastodonUser + guard let account = response.value else { return } + + self?.account = account } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Report/Report/ReportViewController.swift b/Mastodon/Scene/Report/Report/ReportViewController.swift index 468fa5ae6..ae4c3448f 100644 --- a/Mastodon/Scene/Report/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/Report/ReportViewController.swift @@ -84,7 +84,7 @@ extension ReportViewController: ReportReasonViewControllerDelegate { let reportResultViewModel = ReportResultViewModel( context: context, authContext: viewModel.authContext, - user: viewModel.user, + account: viewModel.account, isReported: false ) _ = coordinator.present( @@ -160,7 +160,7 @@ extension ReportViewController: ReportSupplementaryViewControllerDelegate { let reportResultViewModel = ReportResultViewModel( context: context, authContext: viewModel.authContext, - user: viewModel.user, + account: viewModel.account, isReported: true ) diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index ba71da66f..1a21885f9 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -28,7 +28,7 @@ class ReportViewModel { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let status: MastodonStatus? // output @@ -39,17 +39,17 @@ class ReportViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, status: MastodonStatus? ) { self.context = context self.authContext = authContext - self.user = user + self.account = account self.status = status self.reportReasonViewModel = ReportReasonViewModel(context: context) self.reportServerRulesViewModel = ReportServerRulesViewModel(context: context) - self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, user: user, status: status) - self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, user: user) + self.reportStatusViewModel = ReportStatusViewModel(context: context, authContext: authContext, account: account, status: status) + self.reportSupplementaryViewModel = ReportSupplementaryViewModel(context: context, authContext: authContext, account: account) // end init // setup reason viewModel @@ -57,17 +57,8 @@ class ReportViewModel { reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisPost } else { Task { @MainActor in - let managedObjectContext = context.managedObjectContext - let _username: String? = try? await managedObjectContext.perform { - let user = user.object(in: managedObjectContext) - return user?.acctWithDomain - } - if let username = _username { - reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(username) - } else { - reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisAccount - } - } // end Task + reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(account.username) + } } // bind server rules @@ -96,73 +87,63 @@ extension ReportViewModel { func report() async throws { guard !isReporting else { return } - let managedObjectContext = context.managedObjectContext - let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform { - guard let user = self.user.object(in: managedObjectContext) else { return nil } - - // the status picker is essential step in report flow - // only check isSkip or not - let statusIDs: [MastodonStatus.ID]? = { - if self.reportStatusViewModel.isSkip { - let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in - return record.id - } - return _id.flatMap { [$0] } ?? [] - } else { - return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in - return record.id - } + let account = self.account + // the status picker is essential step in report flow + // only check isSkip or not + let statusIDs: [MastodonStatus.ID]? = { + if self.reportStatusViewModel.isSkip { + let _id: MastodonStatus.ID? = self.reportStatusViewModel.status.flatMap { record -> MastodonStatus.ID? in + return record.id } - }() - - // the user comment is essential step in report flow - // only check isSkip or not - let comment: String? = { - let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment - if let comment = _comment, !comment.isEmpty { - return comment - } else { - return nil + return _id.flatMap { [$0] } ?? [] + } else { + return self.reportStatusViewModel.selectStatuses.compactMap { record -> MastodonStatus.ID? in + return record.id } - }() - return Mastodon.API.Reports.FileReportQuery( - accountID: user.id, - statusIDs: statusIDs, - comment: comment, - forward: true, - category: { - switch self.reportReasonViewModel.selectReason { + } + }() + + // the user comment is essential step in report flow + // only check isSkip or not + let comment: String? = { + let _comment = self.reportSupplementaryViewModel.isSkip ? nil : self.reportSupplementaryViewModel.commentContext.comment + if let comment = _comment, !comment.isEmpty { + return comment + } else { + return nil + } + }() + let query = Mastodon.API.Reports.FileReportQuery( + accountID: account.id, + statusIDs: statusIDs, + comment: comment, + forward: true, + category: { + switch self.reportReasonViewModel.selectReason { case .dislike: return nil case .spam: return .spam case .violateRule: return .violation case .other: return .other case .none: return nil - } - }(), - ruleIDs: { - switch self.reportReasonViewModel.selectReason { + } + }(), + ruleIDs: { + switch self.reportReasonViewModel.selectReason { case .violateRule: let ruleIDs = self.reportServerRulesViewModel.selectRules.map { $0.id }.sorted() return ruleIDs default: return nil - } - }() - ) - } - - guard let query = _query else { return } + } + }() + ) do { isReporting = true - #if DEBUG - try await Task.sleep(nanoseconds: .second * 3) - #else let _ = try await context.apiService.report( query: query, authenticationBox: authContext.mastodonAuthenticationBox ) - #endif isReportSuccess = true } catch { isReporting = false diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index d66eadfd7..0b9ab1fba 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -90,7 +90,7 @@ extension ReportResultViewController { do { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: self.viewModel.user + user: self.viewModel.account ) } catch { // handle error @@ -110,7 +110,7 @@ extension ReportResultViewController { do { try await DataSourceFacade.responseToUserMuteAction( dependency: self, - user: self.viewModel.user + account: self.viewModel.account ) } catch { // handle error @@ -130,7 +130,7 @@ extension ReportResultViewController { do { try await DataSourceFacade.responseToUserBlockAction( dependency: self, - user: self.viewModel.user + user: self.viewModel.account ) } catch { // handle error diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index de987b512..70853e184 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -23,7 +23,7 @@ class ReportResultViewModel: ObservableObject { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let isReported: Bool var headline: String { @@ -48,24 +48,20 @@ class ReportResultViewModel: ObservableObject { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, isReported: Bool ) { self.context = context self.authContext = authContext - self.user = user + self.account = account self.isReported = isReported // end init Task { @MainActor in - guard let user = user.object(in: context.managedObjectContext) else { return } - guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return } - self.relationshipViewModel.user = user - self.relationshipViewModel.me = me - - self.avatarURL = user.avatarImageURL() - self.username = user.acctWithDomain + self.avatarURL = account.avatarImageURL() + self.username = account.username + } // end Task } diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift index 0bb2e0cef..0c13b32af 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -68,19 +68,9 @@ extension ReportStatusViewModel.State { Task { let maxID = await viewModel.statusFetchedResultsController.records.last?.id - let managedObjectContext = viewModel.context.managedObjectContext - let _userID: MastodonUser.ID? = try await managedObjectContext.perform { - guard let user = viewModel.user.object(in: managedObjectContext) else { return nil } - return user.id - } - guard let userID = _userID else { - await enter(state: Fail.self) - return - } - do { let response = try await viewModel.context.apiService.userTimeline( - accountID: userID, + accountID: viewModel.account.id, maxID: maxID, sinceID: nil, excludeReplies: true, diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index 186a2806c..2b3713138 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -24,7 +24,7 @@ class ReportStatusViewModel { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let status: MastodonStatus? let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -52,12 +52,12 @@ class ReportStatusViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, status: MastodonStatus? ) { self.context = context self.authContext = authContext - self.user = user + self.account = account self.status = status self.statusFetchedResultsController = StatusFetchedResultsController() // end init diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift index a4239bbc4..d8f9aa783 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift @@ -18,7 +18,7 @@ class ReportSupplementaryViewModel { // Input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let account: Mastodon.Entity.Account let commentContext = ReportItem.CommentContext() @Published var isSkip = false @@ -31,11 +31,11 @@ class ReportSupplementaryViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord + account: Mastodon.Entity.Account ) { self.context = context self.authContext = authContext - self.user = user + self.account = account // end init Publishers.CombineLatest( diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index 7a332ad62..b2fc91781 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -70,10 +70,7 @@ extension SearchResultViewController { status: status ) case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) + assertionFailure() case .hashtag(let tag): await DataSourceFacade.coordinateToHashtagScene( provider: self, diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index 2c4973a30..b1150e3f7 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -217,15 +217,8 @@ extension SettingsCoordinator: ServerDetailsViewControllerDelegate { extension SettingsCoordinator: AboutInstanceViewControllerDelegate { @MainActor func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) { - Task { - let user = try await appContext.apiService.fetchUser(username: account.username, domain: authContext.mastodonAuthenticationBox.domain, authenticationBox: authContext.mastodonAuthenticationBox) - - let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, optionalMastodonUser: user) - - _ = await MainActor.run { - sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show) - } - } + let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, account: account) + sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show) } func sendEmailToAdmin(_ viewController: AboutInstanceViewController, emailAddress: String) { diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 0b1a9db49..f3b301c4f 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -98,7 +98,13 @@ public struct MastodonAuthentication: Codable, Hashable { let userPredicate = MastodonUser.predicate(domain: domain, id: userID) return MastodonUser.findOrFetch(in: context, matching: userPredicate) } - + + public func account() -> Mastodon.Entity.Account? { + // store accounts +#warning("TODO: Implement") + return nil + } + func updating(instance: Instance) -> Self { copy(instanceObjectIdURI: instance.objectID.uriRepresentation()) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index 5e058c258..a1445f71c 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -34,19 +34,6 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - ) - ) - } - return response } @@ -171,7 +158,7 @@ extension APIService { extension APIService { public func fetchUser(username: String, domain: String, authenticationBox: MastodonAuthenticationBox) - async throws -> MastodonUser? { + async throws -> Mastodon.Entity.Account? { let query = Mastodon.API.Account.AccountLookupQuery(acct: "\(username)@\(domain)") let authorization = authenticationBox.userAuthorization @@ -182,21 +169,6 @@ extension APIService { authorization: authorization ).singleOutput() - // user - let managedObjectContext = self.backgroundManagedObjectContext - var result: MastodonUser? - try await managedObjectContext.performChanges { - result = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - ) - ).user - } - - return result + return response.value } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index b3a046bad..dadb34e29 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -145,64 +145,10 @@ extension APIService { return response } - public func toggleShowReblogs( - for user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - - let managedObjectContext = backgroundManagedObjectContext - guard let user = user.object(in: managedObjectContext), - let me = authenticationBox.authentication.user(in: managedObjectContext) - else { throw APIError.implicit(.badRequest) } - - let result: Result, Error> - - let oldShowReblogs = me.showingReblogsBy.contains(user) - let newShowReblogs = (oldShowReblogs == false) - - do { - let response = try await Mastodon.API.Account.follow( - session: session, - domain: authenticationBox.domain, - accountID: user.id, - followQueryType: .follow(query: .init(reblogs: newShowReblogs)), - authorization: authenticationBox.userAuthorization - ).singleOutput() - - result = .success(response) - } catch { - result = .failure(error) - } - - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - - switch result { - case .success(let response): - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isShowingReblogs: oldShowReblogs, by: me) - } - } - - return try result.get() - } - public func toggleShowReblogs( for user: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - - let result: Result, Error> - let relationship = try await Mastodon.API.Account.relationships( session: session, domain: authenticationBox.domain, @@ -213,20 +159,14 @@ extension APIService { let oldShowReblogs = relationship?.showingReblogs == true let newShowReblogs = (oldShowReblogs == false) - do { - let response = try await Mastodon.API.Account.follow( - session: session, - domain: authenticationBox.domain, - accountID: user.id, - followQueryType: .follow(query: .init(reblogs: newShowReblogs)), - authorization: authenticationBox.userAuthorization - ).singleOutput() + let response = try await Mastodon.API.Account.follow( + session: session, + domain: authenticationBox.domain, + accountID: user.id, + followQueryType: .follow(query: .init(reblogs: newShowReblogs)), + authorization: authenticationBox.userAuthorization + ).singleOutput() - result = .success(response) - } catch { - result = .failure(error) - } - - return try result.get() + return response } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index 65f0eb811..d45b33e2e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -14,7 +14,6 @@ import MastodonSDK extension APIService { private struct MastodonMuteContext { - let sourceUserID: MastodonUser.ID let targetUserID: MastodonUser.ID let targetUsername: String let isMuting: Bool @@ -60,33 +59,23 @@ extension APIService { } public func toggleMute( - user: ManagedObjectRecord, - authenticationBox: MastodonAuthenticationBox + authenticationBox: MastodonAuthenticationBox, + account: Mastodon.Entity.Account ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext - let muteContext: MastodonMuteContext = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication + guard let relationship = try await Mastodon.API.Account.relationships( + session: session, + domain: authenticationBox.domain, + query: .init(ids: [account.id]), + authorization: authenticationBox.userAuthorization + ).singleOutput().value.first else { throw APIError.implicit(.badRequest) } + + let muteContext = MastodonMuteContext( + targetUserID: account.id, + targetUsername: account.username, + isMuting: relationship.muting ?? false + ) - guard - let user = user.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { - throw APIError.implicit(.badRequest) - } - - let isMuting = user.mutingBy.contains(me) - - // toggle mute state - user.update(isMuting: !isMuting, by: me) - return MastodonMuteContext( - sourceUserID: me.id, - targetUserID: user.id, - targetUsername: user.username, - isMuting: isMuting - ) - } - let result: Result, Error> do { if muteContext.isMuting { @@ -96,7 +85,7 @@ extension APIService { accountID: muteContext.targetUserID, authorization: authenticationBox.userAuthorization ).singleOutput() - try await getMutes(authenticationBox: authenticationBox) + result = .success(response) } else { let response = try await Mastodon.API.Account.mute( @@ -105,38 +94,16 @@ extension APIService { accountID: muteContext.targetUserID, authorization: authenticationBox.userAuthorization ).singleOutput() - try await getMutes(authenticationBox: authenticationBox) + result = .success(response) } } catch { result = .failure(error) } - - try await managedObjectContext.performChanges { - guard let user = user.object(in: managedObjectContext), - let me = authenticationBox.authentication.user(in: managedObjectContext) - else { return } - - switch result { - case .success(let response): - let relationship = response.value - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: relationship, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isMuting: muteContext.isMuting, by: me) - } - } - + let response = try result.get() return response } - + } From 8918d237cae6dfc921b7135716eea56459c72567 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 8 Dec 2023 13:48:42 +0100 Subject: [PATCH 052/159] Use placeholder for me (IOS-192) --- .../MastodonCore/MastodonAuthentication.swift | 4 +- .../Entity/Mastodon+Entity+Account.swift | 155 ++++++++++++++---- 2 files changed, 122 insertions(+), 37 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index f3b301c4f..6ebfe0d15 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -99,10 +99,10 @@ public struct MastodonAuthentication: Codable, Hashable { return MastodonUser.findOrFetch(in: context, matching: userPredicate) } - public func account() -> Mastodon.Entity.Account? { + public func account() -> Mastodon.Entity.Account { // store accounts #warning("TODO: Implement") - return nil + return Mastodon.Entity.Account.placeholder() } func updating(instance: Instance) -> Self { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index f189a131e..88f05e3fc 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -9,7 +9,7 @@ import Foundation import MastodonCommon extension Mastodon.Entity { - + /// Account /// /// - Since: 0.1.0 @@ -18,8 +18,7 @@ extension Mastodon.Entity { /// 2021/1/28 /// # Reference /// [Document](https://docs.joinmastodon.org/entities/account/) - public final class Account: Codable, Sendable { - + public final class Account: Sendable { public typealias ID = String // Base @@ -27,7 +26,7 @@ extension Mastodon.Entity { public let username: String public let acct: String public let url: String - + // Display public let displayName: String public let note: String @@ -38,50 +37,136 @@ extension Mastodon.Entity { public let locked: Bool public let emojis: [Emoji]? public let discoverable: Bool? - + // Statistical public let createdAt: Date public let lastStatusAt: Date? public let statusesCount: Int public let followersCount: Int public let followingCount: Int - + public let moved: Account? public let fields: [Field]? public let bot: Bool? public let source: Source? public let suspended: Bool? public let muteExpiresAt: Date? - - enum CodingKeys: String, CodingKey { - case id - case username - case acct - case url - - case displayName = "display_name" - case note - case avatar - case avatarStatic = "avatar_static" - case header - case headerStatic = "header_static" - case locked - case emojis - case discoverable - - case createdAt = "created_at" - case lastStatusAt = "last_status_at" - case statusesCount = "statuses_count" - case followersCount = "followers_count" - case followingCount = "following_count" - case moved - - case fields - case bot - case source - case suspended - case muteExpiresAt = "mute_expires_at" + + internal init(id: Mastodon.Entity.Account.ID, username: String, acct: String, url: String, displayName: String, note: String, avatar: String, avatarStatic: String? = nil, header: String, headerStatic: String? = nil, locked: Bool, emojis: [Mastodon.Entity.Emoji]? = nil, discoverable: Bool? = nil, createdAt: Date, lastStatusAt: Date? = nil, statusesCount: Int, followersCount: Int, followingCount: Int, moved: Mastodon.Entity.Account? = nil, fields: [Mastodon.Entity.Field]? = nil, bot: Bool? = nil, source: Mastodon.Entity.Source? = nil, suspended: Bool? = nil, muteExpiresAt: Date? = nil) { + self.id = id + self.username = username + self.acct = acct + self.url = url + self.displayName = displayName + self.note = note + self.avatar = avatar + self.avatarStatic = avatarStatic + self.header = header + self.headerStatic = headerStatic + self.locked = locked + self.emojis = emojis + self.discoverable = discoverable + self.createdAt = createdAt + self.lastStatusAt = lastStatusAt + self.statusesCount = statusesCount + self.followersCount = followersCount + self.followingCount = followingCount + self.moved = moved + self.fields = fields + self.bot = bot + self.source = source + self.suspended = suspended + self.muteExpiresAt = muteExpiresAt } + + @available(*, deprecated, message: "Remove!") + public static func placeholder() -> Mastodon.Entity.Account { + let data = """ +{ + "id": "3006", + "username": "zeitschlag", + "acct": "zeitschlag", + "display_name": "nathan", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2017-04-18T00:00:00.000Z", + "note": "

release-notes-autor.

wer nervt, wird geblockt.

", + "url": "https://chaos.social/@zeitschlag", + "uri": "https://chaos.social/users/zeitschlag", + "avatar": "https://assets.chaos.social/accounts/avatars/000/003/006/original/cf15bb24f41bf74e.jpeg", + "avatar_static": "https://assets.chaos.social/accounts/avatars/000/003/006/original/cf15bb24f41bf74e.jpeg", + "header": "https://assets.chaos.social/accounts/headers/000/003/006/original/5936fa5b2ef78ced.png", + "header_static": "https://assets.chaos.social/accounts/headers/000/003/006/original/5936fa5b2ef78ced.png", + "followers_count": 1347, + "following_count": 235, + "statuses_count": 7484, + "last_status_at": "2023-12-08", + "noindex": false, + "emojis": [], + "roles": [], + "fields": [ + { + "name": "blog", + "value": "https://bullenscheisse.de", + "verified_at": "2023-04-20T13:18:18.930+00:00" + }, + { + "name": "Mastodon", + "value": "https://joinmastodon.org/about", + "verified_at": "2023-11-10T17:06:50.631+00:00" + }, + { + "name": "github", + "value": "https://github.com/zeitschlag/", + "verified_at": "2023-11-10T17:25:59.868+00:00" + }, + { + "name": "German CV", + "value": "https://zeitschlag.net/lebenslauf/", + "verified_at": null + } + ] +} +""".data(using: .utf8)! + + let account = try! Mastodon.API.decoder.decode(Self.self, from: data) + return account + } + } +} + +//MARK: - Codable +extension Mastodon.Entity.Account: Codable { + enum CodingKeys: String, CodingKey { + case id + case username + case acct + case url + + case displayName = "display_name" + case note + case avatar + case avatarStatic = "avatar_static" + case header + case headerStatic = "header_static" + case locked + case emojis + case discoverable + + case createdAt = "created_at" + case lastStatusAt = "last_status_at" + case statusesCount = "statuses_count" + case followersCount = "followers_count" + case followingCount = "following_count" + case moved + + case fields + case bot + case source + case suspended + case muteExpiresAt = "mute_expires_at" } } From 34b962e3ca49ed511a0e3416f4960f159f0f2c72 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 8 Dec 2023 15:15:36 +0100 Subject: [PATCH 053/159] Remove me/remote-profile-viewmodel (IOS-192) --- Mastodon.xcodeproj/project.pbxproj | 8 - Mastodon/Coordinator/SceneCoordinator.swift | 37 ++++- .../Scene/Profile/MeProfileViewModel.swift | 41 ----- Mastodon/Scene/Profile/ProfileViewModel.swift | 6 +- .../Profile/RemoteProfileViewModel.swift | 91 ----------- .../Root/MainTab/MainTabBarController.swift | 53 +++---- Mastodon/Supporting Files/SceneDelegate.swift | 142 ++++++++++-------- 7 files changed, 144 insertions(+), 234 deletions(-) delete mode 100644 Mastodon/Scene/Profile/MeProfileViewModel.swift delete mode 100644 Mastodon/Scene/Profile/RemoteProfileViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e08f34198..82880d0e1 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -427,7 +427,6 @@ DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */; }; DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; - DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */; }; @@ -441,7 +440,6 @@ DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */; }; DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */; }; DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */; }; - DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525632612C988002F1F29 /* MeProfileViewModel.swift */; }; DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; }; DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; }; @@ -1167,7 +1165,6 @@ DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = ""; }; DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; - DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = ""; }; DBB45B5527B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewController.swift; sourceTree = ""; }; DBB45B5827B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = ""; }; @@ -1180,7 +1177,6 @@ DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = ""; }; DBB525552611EDCA002F1F29 /* UserTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewModel.swift; sourceTree = ""; }; DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; - DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = ""; }; DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = ""; }; DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; @@ -2782,8 +2778,6 @@ DBFEEC97279BDC6A004F81DD /* About */, DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */, DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */, - DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */, - DBB525632612C988002F1F29 /* MeProfileViewModel.swift */, ); path = Profile; sourceTree = ""; @@ -3748,7 +3742,6 @@ 0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */, D81A94172B07A1D30067A19D /* ProfileCardView+Configuration.swift in Sources */, DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */, - DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, D82BD7552ABC73AF009A374A /* NotificationPolicyTableViewCell.swift in Sources */, DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */, DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */, @@ -3850,7 +3843,6 @@ DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */, DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */, - DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */, DB3EA8EF281B837000598866 /* DiscoveryCommunityViewController+DataSourceProvider.swift in Sources */, DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */, DB4AA6B327BA34B6009EC082 /* CellFrameCacheContainer.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index f85a7c96f..50f5dfb0f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -49,7 +49,8 @@ final public class SceneCoordinator { appContext.notificationService.requestRevealNotificationPublisher .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] pushNotification in + .sink(receiveValue: { + [weak self] pushNotification in guard let self = self else { return } Task { guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return } @@ -101,20 +102,44 @@ final public class SceneCoordinator { switch type { case .follow: - let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID) - _ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) + Task { + let account = try await appContext.apiService.notification( + notificationID: notificationID, + authenticationBox: authContext.mastodonAuthenticationBox + ).value.account + + let profileViewModel = ProfileViewModel( + context: appContext, + authContext: authContext, + account: account + ) + _ = self.present( + scene: .profile(viewModel: profileViewModel), + from: from, + transition: .show + ) + } case .followRequest: // do nothing break case .mention, .reblog, .favourite, .poll, .status: - let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID) - _ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) + let threadViewModel = RemoteThreadViewModel( + context: appContext, + authContext: authContext, + notificationID: notificationID + ) + _ = self.present( + scene: .thread(viewModel: threadViewModel), + from: from, + transition: .show + ) + case ._other: assertionFailure() break } } // end DispatchQueue.main.async - + } catch { assertionFailure(error.localizedDescription) return diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift deleted file mode 100644 index 0d3e3f383..000000000 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// MeProfileViewModel.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-30. -// - -import UIKit -import Combine -import CoreData -import CoreDataStack -import MastodonCore -import MastodonSDK - -final class MeProfileViewModel: ProfileViewModel { - - @MainActor - init(context: AppContext, authContext: AuthContext) { - let me = authContext.mastodonAuthenticationBox.authentication.account() - super.init( - context: context, - authContext: authContext, - account: me - ) - } - - override func viewDidLoad() { - - super.viewDidLoad() - - Task { - do { - let account = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value - self.account = account - self.me = account - } catch { - // do nothing? - } - } - } -} diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 7a075f6ea..9ff87c669 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -56,10 +56,10 @@ class ProfileViewModel: NSObject { // let needsPagePinToTop = CurrentValueSubject(false) @MainActor - init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account?) { + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account) { self.context = context self.authContext = authContext - self.account = account! + self.account = account self.postsUserTimelineViewModel = UserTimelineViewModel( context: context, authContext: authContext, @@ -87,7 +87,7 @@ class ProfileViewModel: NSObject { // bind user $account .map { user -> UserIdentifier? in - guard let account, let domain = account.domain else { return nil } + guard let domain = account.domain else { return nil } return MastodonUserIdentifier(domain: domain, userID: account.id) } .assign(to: &$userIdentifier) diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift deleted file mode 100644 index 2849e1fe2..000000000 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// RemoteProfileViewModel.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-4-2. -// - -import Foundation -import Combine -import CoreDataStack -import MastodonSDK -import MastodonCore - -final class RemoteProfileViewModel: ProfileViewModel { - - @MainActor - init(context: AppContext, authContext: AuthContext, userID: Mastodon.Entity.Account.ID) { - super.init(context: context, authContext: authContext, account: nil) - - let domain = authContext.mastodonAuthenticationBox.domain - let authorization = authContext.mastodonAuthenticationBox.userAuthorization - Just(userID) - .asyncMap { userID in - try await context.apiService.accountInfo( - domain: domain, - userID: userID, - authorization: authorization - ) - } - .retry(3) - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .failure(_): - // TODO: handle error - break - case .finished: - break - } - } receiveValue: { [weak self] response in - self?.account = response.value - } - .store(in: &disposeBag) - } - - @MainActor - init(context: AppContext, authContext: AuthContext, notificationID: Mastodon.Entity.Notification.ID) { - super.init(context: context, authContext: authContext, account: nil) - - Task { @MainActor in - let response = try await context.apiService.notification( - notificationID: notificationID, - authenticationBox: authContext.mastodonAuthenticationBox - ) - - self.account = response.value.account - } // end Task - } - - @MainActor - init(context: AppContext, authContext: AuthContext, acct: String){ - super.init(context: context, authContext: authContext, account: nil) - - let domain = authContext.mastodonAuthenticationBox.domain - let authenticationBox = authContext.mastodonAuthenticationBox - - Just(acct) - .asyncMap { acct -> Mastodon.Response.Content in - try await context.apiService.search( - query: .init(q: acct, type: .accounts, resolve: true), - authenticationBox: authenticationBox - ).map { $0.accounts.first } - } - .retry(3) - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .failure(_): - // TODO: handle error - break - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let account = response.value else { return } - - self?.account = account - } - .store(in: &disposeBag) - } -} diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 42faa61fc..9e1d082c5 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -95,32 +95,33 @@ class MainTabBarController: UITabBarController { let viewController: UIViewController switch self { - case .home: - let _viewController = HomeTimelineViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .search: - let _viewController = SearchViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .compose: - viewController = UIViewController() - case .notifications: - let _viewController = NotificationViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .me: - let _viewController = ProfileViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = MeProfileViewModel(context: context, authContext: authContext) - viewController = _viewController + case .home: + let _viewController = HomeTimelineViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) + viewController = _viewController + case .search: + let _viewController = SearchViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) + viewController = _viewController + case .compose: + viewController = UIViewController() + case .notifications: + let _viewController = NotificationViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) + viewController = _viewController + case .me: + let me = authContext.mastodonAuthenticationBox.authentication.account() + let _viewController = ProfileViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me) + viewController = _viewController } viewController.title = self.title return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 7c6c33f8e..9ecbfe58a 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -137,16 +137,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { switch (profile, statusID) { case (profile, nil): - let profileViewModel = RemoteProfileViewModel( - context: AppContext.shared, - authContext: authContext, - acct: incomingURL.absoluteString - ) - self.coordinator?.present( - scene: .profile(viewModel: profileViewModel), - from: nil, - transition: .show - ) + Task { + let domain = authContext.mastodonAuthenticationBox.domain + let authenticationBox = authContext.mastodonAuthenticationBox + + guard let account = try await AppContext.shared.apiService.search( + query: .init(q: incomingURL.absoluteString, type: .accounts, resolve: true), + authenticationBox: authenticationBox + ).value.accounts.first else { return } + + let profileViewModel = ProfileViewModel( + context: AppContext.shared, + authContext: authContext, + account: account + ) + _ = self.coordinator?.present( + scene: .profile(viewModel: profileViewModel), + from: nil, + transition: .show + ) + } case (profile, statusID): Task { @@ -248,58 +258,72 @@ extension SceneDelegate { if !UIApplication.shared.canOpenURL(url) { return } - #if DEBUG +#if DEBUG print("source application = \(sendingAppID ?? "Unknown")") print("url = \(url)") - #endif - +#endif + switch url.host { - case "post": - showComposeViewController() - case "profile": - let components = url.pathComponents - guard - components.count == 2, - components[0] == "/", - let authContext = coordinator?.authContext - else { return } - - let profileViewModel = RemoteProfileViewModel( - context: AppContext.shared, - authContext: authContext, - acct: components[1] - ) - self.coordinator?.present( - scene: .profile(viewModel: profileViewModel), - from: nil, - transition: .show - ) - case "status": - let components = url.pathComponents - guard - components.count == 2, - components[0] == "/", - let authContext = coordinator?.authContext - else { return } - let statusId = components[1] - // View post from user - let threadViewModel = RemoteThreadViewModel( - context: AppContext.shared, - authContext: authContext, - statusID: statusId - ) - coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - case "search": - let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems - guard - let authContext = coordinator?.authContext, - let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value - else { return } - - let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery) - coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show) - default: - return + case "post": + showComposeViewController() + case "profile": + let components = url.pathComponents + guard + components.count == 2, + components[0] == "/", + let authContext = coordinator?.authContext + else { return } + + Task { + do { + let domain = authContext.mastodonAuthenticationBox.domain + let authenticationBox = authContext.mastodonAuthenticationBox + + guard let account = try await AppContext.shared.apiService.search( + query: .init(q: components[1], type: .accounts, resolve: true), + authenticationBox: authenticationBox + ).value.accounts.first else { return } + + let profileViewModel = ProfileViewModel( + context: AppContext.shared, + authContext: authContext, + account: account + ) + self.coordinator?.present( + scene: .profile(viewModel: profileViewModel), + from: nil, + transition: .show + ) + } catch { + // fail silently + } + } + case "status": + let components = url.pathComponents + guard + components.count == 2, + components[0] == "/", + let authContext = coordinator?.authContext + else { return } + let statusId = components[1] + // View post from user + let threadViewModel = RemoteThreadViewModel( + context: AppContext.shared, + authContext: authContext, + statusID: statusId + ) + coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + case "search": + let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems + guard + let authContext = coordinator?.authContext, + let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value + else { return } + + let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery) + coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show) + default: + return } } } From 873f282ad102c5a36c5aedc4961eb1239bc45c48 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 8 Dec 2023 15:26:46 +0100 Subject: [PATCH 054/159] Fix fields (IOS-192) --- .../Profile/About/ProfileAboutViewModel.swift | 25 ++++++------------- Mastodon/Scene/Profile/ProfileViewModel.swift | 2 +- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift index 5033dd60c..5b5488b6d 100644 --- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift +++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift @@ -19,7 +19,7 @@ final class ProfileAboutViewModel { // input let context: AppContext - @Published var account: Mastodon.Entity.Account? + @Published var account: Mastodon.Entity.Account @Published var isEditing = false @Published var accountForEdit: Mastodon.Entity.Account? @@ -32,24 +32,13 @@ final class ProfileAboutViewModel { @Published var emojiMeta: MastodonContent.Emojis = [:] @Published var createdAt: Date = Date() - init(context: AppContext) { + init(context: AppContext, account: Mastodon.Entity.Account) { + self.account = account self.context = context -#warning("TODO: Implement") -// $account -// .compactMap { $0 } -// .flatMap { $0.publisher(for: \.emojis) } -// .map { $0.asDictionary } -// .assign(to: &$emojiMeta) -// -// $account -// .compactMap { $0 } -// .flatMap { $0.publisher(for: \.fields) } -// .assign(to: &$fields) -// -// $account -// .compactMap { $0 } -// .flatMap { $0.publisher(for: \.createdAt) } -// .assign(to: &$createdAt) + + emojiMeta = account.emojiMeta + fields = account.mastodonFields + createdAt = account.createdAt Publishers.CombineLatest( $fields, diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 9ff87c669..01b29a46f 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -78,7 +78,7 @@ class ProfileViewModel: NSObject { title: L10n.Scene.Profile.SegmentedControl.media, queryFilter: .init(onlyMedia: true) ) - self.profileAboutViewModel = ProfileAboutViewModel(context: context) + self.profileAboutViewModel = ProfileAboutViewModel(context: context, account: account) super.init() // bind me From d3c7ba2c7cd24e7e5f82b20552e557e9fbbb7d61 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 8 Dec 2023 15:43:06 +0100 Subject: [PATCH 055/159] Show header-meta-information about account (IOS-192) --- .../Header/ProfileHeaderViewController.swift | 15 ++---- .../Header/ProfileHeaderViewModel.swift | 7 +-- .../ProfileHeaderView+Configuration.swift | 47 ++++--------------- .../Scene/Profile/ProfileViewController.swift | 4 +- .../Entity/Mastodon+Entity+Account.swift | 5 ++ 5 files changed, 25 insertions(+), 53 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index aba92c9c8..8b5a0c8b8 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -88,7 +88,11 @@ extension ProfileHeaderViewController { override func viewDidLoad() { super.viewDidLoad() - + + profileHeaderView.prepareForReuse() + profileHeaderView.configuration(account: viewModel.account) + + view.setContentHuggingPriority(.required - 1, for: .vertical) view.backgroundColor = .systemBackground @@ -128,15 +132,6 @@ extension ProfileHeaderViewController { self.titleView.subtitleLabel.alpha = isTitleViewContentOffsetDidSet ? 1 : 0 } .store(in: &disposeBag) - viewModel.$account - .receive(on: DispatchQueue.main) - .sink { [weak self] account in - guard let self, let account else { return } - - self.profileHeaderView.prepareForReuse() - self.profileHeaderView.configuration(account: account) - } - .store(in: &disposeBag) viewModel.$relationship .assign(to: \.relationship, on: profileHeaderView.viewModel) .store(in: &disposeBag) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 726ddf715..f8db96e06 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -26,7 +26,7 @@ final class ProfileHeaderViewModel { let context: AppContext let authContext: AuthContext - @Published var account: Mastodon.Entity.Account? + @Published var account: Mastodon.Entity.Account @Published var relationship: Mastodon.Entity.Relationship? @Published var isMyself = false @@ -44,10 +44,11 @@ final class ProfileHeaderViewModel { @Published var isTitleViewDisplaying = false @Published var isTitleViewContentOffsetSet = false - init(context: AppContext, authContext: AuthContext) { + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account) { self.context = context self.authContext = authContext - + self.account = account + $accountForEdit .receive(on: DispatchQueue.main) .sink { [weak self] account in diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift index b044b0603..2d8a6cdd0 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift @@ -11,44 +11,15 @@ import MastodonSDK extension ProfileHeaderView { func configuration(account: Mastodon.Entity.Account) { - #warning("TODO: Implement") -// // header -// account.header.publisher -// .assign(to: \.headerImageURL, on: viewModel) -// .store(in: &disposeBag) -// // avatar -// account.avatar.publisher -// .assign(to: \.avatarImageURL, on: viewModel) -// .store(in: &disposeBag) -// // emojiMeta -// account.emojis.publisher -// .map { $0.asDictionary } -// .assign(to: \.emojiMeta, on: viewModel) -// .store(in: &disposeBag) -// // name -// account.publisher(for: \.displayName) -// .map { _ in account.displayNameWithFallback } -// .assign(to: \.name, on: viewModel) -// .store(in: &disposeBag) -// // username -// viewModel.acct = account.acctWithDomain -// // bio -// account.publisher(for: \.note) -// .assign(to: \.note, on: viewModel) -// .store(in: &disposeBag) -// // dashboard -// account.publisher(for: \.statusesCount) -// .map { Int($0) } -// .assign(to: \.statusesCount, on: viewModel) -// .store(in: &disposeBag) -// account.publisher(for: \.followingCount) -// .map { Int($0) } -// .assign(to: \.followingCount, on: viewModel) -// .store(in: &disposeBag) -// account.publisher(for: \.followersCount) -// .map { Int($0) } -// .assign(to: \.followersCount, on: viewModel) -// .store(in: &disposeBag) + viewModel.headerImageURL = account.headerImageURL() + viewModel.avatarImageURL = account.avatarImageURL() + viewModel.emojiMeta = account.emojiMeta + viewModel.name = account.displayNameWithFallback + viewModel.acct = account.acctWithDomain + viewModel.note = account.note + viewModel.statusesCount = account.statusesCount + viewModel.followingCount = account.followingCount + viewModel.followersCount = account.followersCount } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index f9b6c7ec4..b43694483 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -122,7 +122,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi let viewController = ProfileHeaderViewController() viewController.context = context viewController.coordinator = coordinator - viewController.viewModel = ProfileHeaderViewModel(context: context, authContext: viewModel.authContext) + viewController.viewModel = ProfileHeaderViewModel(context: context, authContext: viewModel.authContext, account: viewModel.account) return viewController }() @@ -378,7 +378,7 @@ extension ProfileViewController { profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear ) .sink { [weak self] (user, _) in - guard let self, let user else { return } + guard let self else { return } Task { _ = try await self.context.apiService.fetchUser( username: user.username, diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 88f05e3fc..2f5b40acd 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -216,6 +216,11 @@ extension Mastodon.Entity.Account { return components.host } + public func headerImageURL() -> URL? { + let string = UserDefaults.shared.preferredStaticAvatar ? headerStatic ?? header : header + return URL(string: string) + } + public func avatarImageURL() -> URL? { let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar return URL(string: string) From 60aafe6330e429a7208c9a21d2bff64ec55727a4 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 13 Dec 2023 15:07:16 +0100 Subject: [PATCH 056/159] Add JSON-based account-persistence (IOS-192) This is per user. Also: Fetch authenticated accounts regularly Also: Move Persistence-stuff to MastodonCore because. --- Mastodon.xcodeproj/project.pbxproj | 20 ------- .../Extension/AppContext+NextAccount.swift | 2 - .../Persistence/Model/SearchHistory.swift | 25 --------- ...Provider+StatusTableViewCellDelegate.swift | 20 +++---- ...omeTimelineViewModel+LoadLatestState.swift | 1 + ...omeTimelineViewModel+LoadOldestState.swift | 3 + .../HomeTimeline/HomeTimelineViewModel.swift | 4 +- .../Root/MainTab/MainTabBarController.swift | 20 ++----- .../Sources/MastodonCore/AppContext.swift | 2 - .../AuthenticationServiceProvider.swift | 29 ++++++++-- .../FeedFetchedResultsController.swift | 20 ++++--- .../MastodonCore/MastodonAuthentication.swift | 8 +-- .../Persistence/FileManager+Account.swift | 55 +++++++++++++++++++ .../FileManager+SearchHistory.swift | 14 ++--- .../Persistence/FileManager+Timeline.swift | 19 +++---- .../Persistence/Model/SearchHistory.swift | 31 +++++++++++ .../Persistence/Persistence.swift | 11 +++- .../Service/API/APIService+Following.swift | 23 -------- .../Service/API/APIService+HomeTimeline.swift | 14 +---- .../Service/AuthenticationService.swift | 4 +- 20 files changed, 172 insertions(+), 153 deletions(-) delete mode 100644 Mastodon/Persistence/Model/SearchHistory.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/FileManager+SearchHistory.swift (81%) rename {Mastodon => MastodonSDK/Sources/MastodonCore}/Persistence/FileManager+Timeline.swift (72%) create mode 100644 MastodonSDK/Sources/MastodonCore/Persistence/Model/SearchHistory.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 82880d0e1..2abbab917 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -60,7 +60,6 @@ 2AE202AC297FE19100F66E55 /* FollowersCountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */; }; 2AE202AD297FE1CD00F66E55 /* WidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A72813E297EC762004138C5 /* WidgetExtension.swift */; }; 2AE244482927831100BDBF7C /* UIImage+SFSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */; }; - 2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; @@ -162,8 +161,6 @@ D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; }; D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; }; D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; }; - D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */; }; - D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AC98782B0F622B0045EC2B /* SearchHistory.swift */; }; D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; }; D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; }; D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; }; @@ -694,7 +691,6 @@ 2AE202A9297FDDF500F66E55 /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; 2AE202AB297FE19100F66E55 /* FollowersCountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersCountIntentHandler.swift; sourceTree = ""; }; 2AE244472927831100BDBF7C /* UIImage+SFSymbols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SFSymbols.swift"; sourceTree = ""; }; - 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Timeline.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -833,8 +829,6 @@ D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = ""; }; D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = ""; }; D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; - D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SearchHistory.swift"; sourceTree = ""; }; - D8AC98782B0F622B0045EC2B /* SearchHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistory.swift; sourceTree = ""; }; D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewToggleCell.swift; sourceTree = ""; }; D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewCell.swift; sourceTree = ""; }; D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; @@ -1892,21 +1886,10 @@ D8AC98742B0F615E0045EC2B /* Persistence */ = { isa = PBXGroup; children = ( - D8AC98772B0F62230045EC2B /* Model */, - D8AC98752B0F61680045EC2B /* FileManager+SearchHistory.swift */, - 2AF2E7BE2B19DC6E00D98917 /* FileManager+Timeline.swift */, ); path = Persistence; sourceTree = ""; }; - D8AC98772B0F62230045EC2B /* Model */ = { - isa = PBXGroup; - children = ( - D8AC98782B0F622B0045EC2B /* SearchHistory.swift */, - ); - path = Model; - sourceTree = ""; - }; D8E5C347296DB896007E76A7 /* Edit History */ = { isa = PBXGroup; children = ( @@ -3856,7 +3839,6 @@ 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, - D8AC98792B0F622B0045EC2B /* SearchHistory.swift in Sources */, DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */, DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */, @@ -3864,7 +3846,6 @@ DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */, DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */, DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, - D8AC98762B0F61680045EC2B /* FileManager+SearchHistory.swift in Sources */, DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */, DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */, DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */, @@ -3969,7 +3950,6 @@ DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */, DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */, - 2AF2E7BF2B19DC6E00D98917 /* FileManager+Timeline.swift in Sources */, DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */, DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */, DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */, diff --git a/Mastodon/Extension/AppContext+NextAccount.swift b/Mastodon/Extension/AppContext+NextAccount.swift index db3df4194..a2b0b34b3 100644 --- a/Mastodon/Extension/AppContext+NextAccount.swift +++ b/Mastodon/Extension/AppContext+NextAccount.swift @@ -5,8 +5,6 @@ // Created by Marcus Kida on 17.11.22. // -import CoreData -import CoreDataStack import MastodonCore import MastodonSDK diff --git a/Mastodon/Persistence/Model/SearchHistory.swift b/Mastodon/Persistence/Model/SearchHistory.swift deleted file mode 100644 index 057536747..000000000 --- a/Mastodon/Persistence/Model/SearchHistory.swift +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright © 2023 Mastodon gGmbH. All rights reserved. - -import Foundation -import MastodonCore -import MastodonSDK - -extension Persistence.SearchHistory { - struct Item: Codable, Hashable, Equatable { - let updatedAt: Date - let userID: Mastodon.Entity.Account.ID - - let account: Mastodon.Entity.Account? - let hashtag: Mastodon.Entity.Tag? - - func hash(into hasher: inout Hasher) { - hasher.combine(userID) - hasher.combine(account) - hasher.combine(hashtag) - } - - public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool { - return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID - } - } -} diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 115f077c4..0edb9e0cd 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -39,21 +39,19 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte break case .reply: guard let replyToAccountID = status.entity.inReplyToAccountID else { return } - #warning("TODO: Implement Domain") - await DataSourceFacade.coordinateToProfileScene(provider: self, - domain: "", - accountID: replyToAccountID) + await DataSourceFacade.coordinateToProfileScene(provider: self, + domain: domain, + accountID: replyToAccountID) - case .repost: - await DataSourceFacade.coordinateToProfileScene( - provider: self, - target: .reblog, // keep the wrapper for header author - status: status - ) + case .repost: + await DataSourceFacade.coordinateToProfileScene( + provider: self, + target: .reblog, // keep the wrapper for header author + status: status + ) } } } - } // MARK: - avatar button diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index 19a58c80b..c24b2dbe3 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -92,6 +92,7 @@ extension HomeTimelineViewModel.LoadLatestState { } do { + await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService) let response = try await viewModel.context.apiService.homeTimeline( authenticationBox: viewModel.authContext.mastodonAuthenticationBox ) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 5f306ea20..fa419635f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -8,6 +8,7 @@ import Foundation import GameplayKit import MastodonSDK +import MastodonCore extension HomeTimelineViewModel { class LoadOldestState: GKState { @@ -60,6 +61,8 @@ extension HomeTimelineViewModel.LoadOldestState { } do { + await AuthenticationServiceProvider.shared.fetchAccounts(apiService: viewModel.context.apiService) + let response = try await viewModel.context.apiService.homeTimeline( maxID: maxID, authenticationBox: viewModel.authContext.mastodonAuthenticationBox diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 70aafb730..898f091f8 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -147,7 +147,9 @@ extension HomeTimelineViewModel { // reconfigure item snapshot.reconfigureItems([item]) await updateSnapshotUsingReloadData(snapshot: snapshot) - + + await AuthenticationServiceProvider.shared.fetchAccounts(apiService: context.apiService) + // fetch data let maxID = status.id _ = try? await context.apiService.homeTimeline( diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 9e1d082c5..258585c1d 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -89,7 +89,7 @@ class MainTabBarController: UITabBarController { @MainActor func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) async -> UIViewController { - guard let authContext = authContext else { + guard let authContext, let me = authContext.mastodonAuthenticationBox.authentication.account() else { return UITableViewController() } @@ -116,7 +116,6 @@ class MainTabBarController: UITabBarController { _viewController.viewModel = .init(context: context, authContext: authContext) viewController = _viewController case .me: - let me = authContext.mastodonAuthenticationBox.authentication.account() let _viewController = ProfileViewController() _viewController.context = context _viewController.coordinator = coordinator @@ -133,7 +132,6 @@ class MainTabBarController: UITabBarController { private(set) var isReadyForWizardAvatarButton = false // output - var avatarURLObserver: AnyCancellable? @Published var avatarURL: URL? // haptic feedback @@ -268,28 +266,20 @@ extension MainTabBarController { NotificationCenter.default.publisher(for: .userFetched) .receive(on: DispatchQueue.main) .sink { [weak self] _ in - guard let self = self else { return } - if let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) { - self.avatarURLObserver = user.publisher(for: \.avatar) - .sink { [weak self, weak user] _ in - guard let self = self else { return } - guard let user = user else { return } - guard user.managedObjectContext != nil else { return } - self.avatarURL = user.avatarImageURL() - } + 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(user.displayNameWithFallback) + profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(account.displayNameWithFallback) self.context.authenticationService.updateActiveUserAccountPublisher .sink { [weak self] in self?.updateUserAccount() } .store(in: &self.disposeBag) - } else { - self.avatarURLObserver = nil } } .store(in: &disposeBag) diff --git a/MastodonSDK/Sources/MastodonCore/AppContext.swift b/MastodonSDK/Sources/MastodonCore/AppContext.swift index c1e8933ee..1f83b73d8 100644 --- a/MastodonSDK/Sources/MastodonCore/AppContext.swift +++ b/MastodonSDK/Sources/MastodonCore/AppContext.swift @@ -122,8 +122,6 @@ public class AppContext: ObservableObject { } .store(in: &disposeBag) } - - } extension AppContext { diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index ec706cc98..1f120e522 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -54,21 +54,21 @@ public extension AuthenticationServiceProvider { func getAuthentication(matching userAccessToken: String) -> MastodonAuthentication? { authentications.first(where: { $0.userAccessToken == userAccessToken }) } - + func authenticationSortedByActivation() -> [MastodonAuthentication] { // fixme: why do we need this? return authentications.sorted(by: { $0.activedAt > $1.activedAt }) } - + func restore() { authentications = Self.keychain.allKeys().compactMap { guard let encoded = Self.keychain[$0], - let data = Data(base64Encoded: encoded) + let data = Data(base64Encoded: encoded) else { return nil } return try? JSONDecoder().decode(MastodonAuthentication.self, from: data) } } - + func migrateLegacyAuthentications(in context: NSManagedObjectContext) { do { let legacyAuthentications = try context.fetch(MastodonAuthenticationLegacy.sortedFetchRequest) @@ -101,10 +101,29 @@ public extension AuthenticationServiceProvider { logger.log(level: .error, "Could not migrate legacy authentications") } } - + var authenticationMigrationRequired: Bool { userDefaults.didMigrateAuthentications == false } + + func fetchAccounts(apiService: APIService, completion: (() -> Void)? = nil) async { + // FIXME: This is a dirty hack to make the performance-stuff work. + // Problem is, that we don't persist the user on disk anymore. So we have to fetch + // it when we need it to display on the home timeline. + // We need this (also) for the Account-list, but it might be the wrong place. App Startup might be more appropriate + for authentication in authentications { + guard let account = try? await apiService.accountInfo(domain: authentication.domain, + userID: authentication.userID, + authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value else { continue } + + FileManager.default.store(account: account, forUserID: authentication.userID) + } + + NotificationCenter.default.post(name: .userFetched, object: nil) + + completion?() + } + } // MARK: - Private diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 77bf8098a..ee2a1ac9b 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -10,6 +10,7 @@ import Foundation import UIKit import Combine import MastodonSDK +import MastodonCore final public class FeedFetchedResultsController { @@ -80,15 +81,16 @@ final public class FeedFetchedResultsController { private extension FeedFetchedResultsController { 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) } + 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) } } } } diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 6ebfe0d15..317e9ad52 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -99,10 +99,10 @@ public struct MastodonAuthentication: Codable, Hashable { return MastodonUser.findOrFetch(in: context, matching: userPredicate) } - public func account() -> Mastodon.Entity.Account { - // store accounts -#warning("TODO: Implement") - return Mastodon.Entity.Account.placeholder() + public func account() -> Mastodon.Entity.Account? { + let account = FileManager.default.accounts(forUserID: userID).first(where: { $0.id == userID }) + + return account } func updating(instance: Instance) -> Self { diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift new file mode 100644 index 000000000..1df3aef1e --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift @@ -0,0 +1,55 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import MastodonSDK + +extension FileManager { + public func store(account: Mastodon.Entity.Account, forUserID userID: String) { + // store accounts for each loged in user + var accounts = accounts(forUserID: userID) + + if let index = accounts.firstIndex(of: account) { + accounts.remove(at: index) + } + + accounts.append(account) + + storeJSON(accounts, userID: userID) + } + + public func accounts(forUserID userID: String) -> [Mastodon.Entity.Account] { + guard let documentsDirectory else { return [] } + + let accountPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory) + + guard let data = try? Data(contentsOf: accountPath) else { return [] } + + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .iso8601 + + do { + let accounts = try jsonDecoder.decode([Mastodon.Entity.Account].self, from: data) + return accounts + } catch { + return [] + } + + } + + private func storeJSON(_ encodable: Encodable, userID: String) { + guard let documentsDirectory else { return } + + let jsonEncoder = JSONEncoder() + jsonEncoder.dateEncodingStrategy = .iso8601 + do { + let data = try jsonEncoder.encode(encodable) + + let accountsPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory) + try data.write(to: accountsPath) + } catch { + debugPrint(error.localizedDescription) + } + + } + +} diff --git a/Mastodon/Persistence/FileManager+SearchHistory.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift similarity index 81% rename from Mastodon/Persistence/FileManager+SearchHistory.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift index 95dd29a52..e289044d3 100644 --- a/Mastodon/Persistence/FileManager+SearchHistory.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift @@ -4,11 +4,11 @@ import Foundation import MastodonCore extension FileManager { - func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] { + public func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] { return try searchItems().filter { $0.userID == userID } } - func searchItems() throws -> [Persistence.SearchHistory.Item] { + public func searchItems() throws -> [Persistence.SearchHistory.Item] { guard let documentsDirectory else { return [] } let searchHistoryPath = Persistence.searchHistory.filepath(baseURL: documentsDirectory) @@ -28,9 +28,7 @@ extension FileManager { } } - func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws { - guard let documentsDirectory else { return } - + public func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws { var searchItems = (try? searchItems()) ?? [] if let index = searchItems.firstIndex(of: newSearchItem) { @@ -58,10 +56,8 @@ extension FileManager { } - func removeSearchHistory(forUser userID: String) { - guard let documentsDirectory else { return } - - var searchItems = (try? searchItems()) ?? [] + public func removeSearchHistory(forUser userID: String) { + let searchItems = (try? searchItems()) ?? [] let newSearchItems = searchItems.filter { $0.userID != userID } storeJSON(newSearchItems, .searchHistory) diff --git a/Mastodon/Persistence/FileManager+Timeline.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift similarity index 72% rename from Mastodon/Persistence/FileManager+Timeline.swift rename to MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift index 0a7046eaa..695686d6f 100644 --- a/Mastodon/Persistence/FileManager+Timeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift @@ -1,22 +1,21 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import Foundation -import MastodonCore import MastodonSDK extension FileManager { private static let cacheItemsLimit: Int = 100 // max number of items to cache // Retrieve - func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] { + public func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] { try cached(timeline: .homeTimeline(userId)).map(MastodonStatus.fromEntity) } - func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { + public func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { try cached(timeline: .notificationsAll(userId)) } - func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { + public func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { try cached(timeline: .notificationsMentions(userId)) } @@ -38,15 +37,15 @@ extension FileManager { } // Create - func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) { + public func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) { cache(items.map { $0.entity }, timeline: .homeTimeline(userIdentifier)) } - func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { + public func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { cache(items, timeline: .notificationsAll(userIdentifier)) } - func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { + public func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { cache(items, timeline: .notificationsMentions(userIdentifier)) } @@ -71,15 +70,15 @@ extension FileManager { } // Delete - func invalidateHomeTimelineCache(for userId: UserIdentifier) { + public func invalidateHomeTimelineCache(for userId: UserIdentifier) { invalidate(timeline: .homeTimeline(userId)) } - func invalidateNotificationsAll(for userId: UserIdentifier) { + public func invalidateNotificationsAll(for userId: UserIdentifier) { invalidate(timeline: .notificationsAll(userId)) } - func invalidateNotificationsMentions(for userId: UserIdentifier) { + public func invalidateNotificationsMentions(for userId: UserIdentifier) { invalidate(timeline: .notificationsMentions(userId)) } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Model/SearchHistory.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Model/SearchHistory.swift new file mode 100644 index 000000000..9ad5b30be --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Model/SearchHistory.swift @@ -0,0 +1,31 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import MastodonSDK + +extension Persistence.SearchHistory { + public struct Item: Codable, Hashable, Equatable { + public let updatedAt: Date + public let userID: Mastodon.Entity.Account.ID + + public let account: Mastodon.Entity.Account? + public let hashtag: Mastodon.Entity.Tag? + + public init(updatedAt: Date, userID: Mastodon.Entity.Account.ID, account: Mastodon.Entity.Account?, hashtag: Mastodon.Entity.Tag?) { + self.updatedAt = updatedAt + self.userID = userID + self.account = account + self.hashtag = hashtag + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(userID) + hasher.combine(account) + hasher.combine(hashtag) + } + + public static func == (lhs: Persistence.SearchHistory.Item, rhs: Persistence.SearchHistory.Item) -> Bool { + return lhs.account == rhs.account && lhs.hashtag == rhs.hashtag && lhs.userID == rhs.userID + } + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index f11ff61e1..ffb45a26f 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -13,7 +13,12 @@ public enum Persistence { case homeTimeline(UserIdentifier) case notificationsMentions(UserIdentifier) case notificationsAll(UserIdentifier) - + case accounts(userID: String) + + private func uniqueUserDomainIdentifier(for userIdentifier: UserIdentifier) -> String { + "\(userIdentifier.userID)@\(userIdentifier.domain)" + } + private var filename: String { switch self { case .searchHistory: @@ -23,7 +28,9 @@ public enum Persistence { case let .notificationsMentions(userIdentifier): return "notifications_mentions_\(userIdentifier.uniqueUserDomainIdentifier)" case let .notificationsAll(userIdentifier): - return "notifications_all_\(userIdentifier.uniqueUserDomainIdentifier)" + return "notifications_all_\(uniqueUserDomainIdentifier(for: userIdentifier))" + case .accounts(let userID): + return "account_\(userID)" } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift index 683a98166..749d09110 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Following.swift @@ -34,29 +34,6 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - - for entity in response.value { - let result = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - - if let me = me { - let user = result.user - user.update(isFollowing: true, by: me) - } - } - - } - return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 272d81fa2..1b32b88aa 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -33,7 +33,7 @@ extension APIService { limit: limit, local: local ) - + let response = try await Mastodon.API.Timeline.home( session: session, domain: domain, @@ -54,18 +54,6 @@ extension APIService { ) } } - - // FIXME: This is a dirty hack to make the performance-stuff work. - // Problem is, that we don't persist the user on disk anymore. So we have to fetch - // it when we need it to display on the home timeline. - // We need this (also) for the Account-list, but it might be the wrong place. App Startup might be more appropriate - for authentication in AuthenticationServiceProvider.shared.authentications { - _ = try? await accountInfo(domain: authentication.domain, - userID: authentication.userID, - authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value - } - - NotificationCenter.default.post(name: .userFetched, object: nil) return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index 8c7963a19..e43b812c9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -25,13 +25,13 @@ public final class AuthenticationService: NSObject { // output @Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = [] - + private func fetchFollowedBlockedUserIds( _ authBox: MastodonAuthenticationBox, _ previousFollowingIDs: [String]? = nil, _ maxID: String? = nil ) async throws { - guard let apiService = apiService else { return } + guard let apiService else { return } let followingResponse = try await fetchFollowing(maxID, apiService, authBox) let followingIds = (previousFollowingIDs ?? []) + followingResponse.ids From 9046b8b8c8d0a561643487a02e058b6921dcf67d Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 13 Dec 2023 16:15:55 +0100 Subject: [PATCH 057/159] Remove placeholder (IOS-192) --- .../Entity/Mastodon+Entity+Account.swift | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 2f5b40acd..a358bcace 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -78,62 +78,6 @@ extension Mastodon.Entity { self.suspended = suspended self.muteExpiresAt = muteExpiresAt } - - @available(*, deprecated, message: "Remove!") - public static func placeholder() -> Mastodon.Entity.Account { - let data = """ -{ - "id": "3006", - "username": "zeitschlag", - "acct": "zeitschlag", - "display_name": "nathan", - "locked": false, - "bot": false, - "discoverable": true, - "group": false, - "created_at": "2017-04-18T00:00:00.000Z", - "note": "

release-notes-autor.

wer nervt, wird geblockt.

", - "url": "https://chaos.social/@zeitschlag", - "uri": "https://chaos.social/users/zeitschlag", - "avatar": "https://assets.chaos.social/accounts/avatars/000/003/006/original/cf15bb24f41bf74e.jpeg", - "avatar_static": "https://assets.chaos.social/accounts/avatars/000/003/006/original/cf15bb24f41bf74e.jpeg", - "header": "https://assets.chaos.social/accounts/headers/000/003/006/original/5936fa5b2ef78ced.png", - "header_static": "https://assets.chaos.social/accounts/headers/000/003/006/original/5936fa5b2ef78ced.png", - "followers_count": 1347, - "following_count": 235, - "statuses_count": 7484, - "last_status_at": "2023-12-08", - "noindex": false, - "emojis": [], - "roles": [], - "fields": [ - { - "name": "blog", - "value": "https://bullenscheisse.de", - "verified_at": "2023-04-20T13:18:18.930+00:00" - }, - { - "name": "Mastodon", - "value": "https://joinmastodon.org/about", - "verified_at": "2023-11-10T17:06:50.631+00:00" - }, - { - "name": "github", - "value": "https://github.com/zeitschlag/", - "verified_at": "2023-11-10T17:25:59.868+00:00" - }, - { - "name": "German CV", - "value": "https://zeitschlag.net/lebenslauf/", - "verified_at": null - } - ] -} -""".data(using: .utf8)! - - let account = try! Mastodon.API.decoder.decode(Self.self, from: data) - return account - } } } From f51d5b7fe2061d290a8bf2438c3b775bc1164c5e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 13 Dec 2023 16:42:35 +0100 Subject: [PATCH 058/159] Replace MastodonUser.ID with string (IOS-192) --- .../Provider/DataSourceFacade+Follow.swift | 2 +- .../Entity/Mastodon/MastodonUser.swift | 20 +++++++++---------- .../Entity/Mastodon/Status.swift | 8 ++++---- .../CoreDataStack/Entity/Transient/Acct.swift | 2 +- .../MastodonAuthenticationBox.swift | 4 ++-- .../CoreDataStack/Notification+Property.swift | 2 +- .../Service/API/APIService+Block.swift | 4 ++-- .../Service/API/APIService+Follow.swift | 4 ++-- .../Service/API/APIService+Mute.swift | 2 +- .../Service/API/APIService+Relationship.swift | 4 ++-- .../Service/AuthenticationService.swift | 2 +- 11 files changed, 26 insertions(+), 28 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index c0b4cca77..84deba2ca 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -56,7 +56,7 @@ extension DataSourceFacade { await selectionFeedbackGenerator.selectionChanged() let managedObjectContext = dependency.context.managedObjectContext - let _userID: MastodonUser.ID? = try await managedObjectContext.perform { + let _userID: String? = try await managedObjectContext.perform { return notification.account.id } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 8e0ef8a47..7080ec3fd 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -11,15 +11,13 @@ import Foundation /// See also `CoreDataStack.MastodonUser`, this extension contains several @available(*, deprecated, message: "Replace with Mastodon.Entity.Account") final public class MastodonUser: NSManagedObject { - - public typealias ID = String - + // sourcery: autoGenerateProperty - @NSManaged public private(set) var identifier: ID + @NSManaged public private(set) var identifier: String // sourcery: autoGenerateProperty @NSManaged public private(set) var domain: String // sourcery: autoGenerateProperty - @NSManaged public private(set) var id: ID + @NSManaged public private(set) var id: String // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var acct: String @@ -206,11 +204,11 @@ extension MastodonUser { ]) } - public static func predicate(followingBy userID: MastodonUser.ID) -> NSPredicate { + public static func predicate(followingBy userID: String) -> NSPredicate { NSPredicate(format: "ANY %K.%K == %@", #keyPath(MastodonUser.followingBy), #keyPath(MastodonUser.id), userID) } - public static func predicate(followRequestedBy userID: MastodonUser.ID) -> NSPredicate { + public static func predicate(followRequestedBy userID: String) -> NSPredicate { NSPredicate(format: "ANY %K.%K == %@", #keyPath(MastodonUser.followRequestedBy), #keyPath(MastodonUser.id), userID) } @@ -223,9 +221,9 @@ extension MastodonUser: AutoGenerateProperty { // Generated using Sourcery // DO NOT EDIT public struct Property { - public let identifier: ID + public let identifier: String public let domain: String - public let id: ID + public let id: String public let acct: String public let username: String public let displayName: String @@ -247,9 +245,9 @@ extension MastodonUser: AutoGenerateProperty { public let fields: [MastodonField] public init( - identifier: ID, + identifier: String, domain: String, - id: ID, + id: String, acct: String, username: String, displayName: String, diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift index 1b457f085..8f6b95bf4 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift @@ -64,7 +64,7 @@ public final class Status: NSManagedObject { // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var inReplyToID: Status.ID? // sourcery: autoUpdatableObject, autoGenerateProperty - @NSManaged public private(set) var inReplyToAccountID: MastodonUser.ID? + @NSManaged public private(set) var inReplyToAccountID: String? // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var language: String? // (ISO 639 Part 1 two-letter language code) @@ -270,7 +270,7 @@ extension Status: AutoGenerateProperty { public let repliesCount: Int64 public let url: String? public let inReplyToID: Status.ID? - public let inReplyToAccountID: MastodonUser.ID? + public let inReplyToAccountID: String? public let language: String? public let text: String? public let updatedAt: Date @@ -295,7 +295,7 @@ extension Status: AutoGenerateProperty { repliesCount: Int64, url: String?, inReplyToID: Status.ID?, - inReplyToAccountID: MastodonUser.ID?, + inReplyToAccountID: String?, language: String?, text: String?, updatedAt: Date, @@ -484,7 +484,7 @@ extension Status: AutoUpdatableObject { self.inReplyToID = inReplyToID } } - public func update(inReplyToAccountID: MastodonUser.ID?) { + public func update(inReplyToAccountID: String?) { if self.inReplyToAccountID != inReplyToAccountID { self.inReplyToAccountID = inReplyToAccountID } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/Acct.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/Acct.swift index fe59bb9d4..e7e071da0 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/Acct.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/Acct.swift @@ -11,7 +11,7 @@ import Foundation extension Feed { public enum Acct: RawRepresentable { case none - case mastodon(domain: String, userID: MastodonUser.ID) + case mastodon(domain: String, userID: String) public init?(rawValue: String) { let components = rawValue.split(separator: "@", maxSplits: 2) diff --git a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift index 8db326dfe..0a62d99d2 100644 --- a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift +++ b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift @@ -12,7 +12,7 @@ import MastodonSDK public struct MastodonAuthenticationBox: UserIdentifier { public let authentication: MastodonAuthentication public let domain: String - public let userID: MastodonUser.ID + public let userID: String public let appAuthorization: Mastodon.API.OAuth.Authorization public let userAuthorization: Mastodon.API.OAuth.Authorization public let inMemoryCache: MastodonAccountInMemoryCache @@ -20,7 +20,7 @@ public struct MastodonAuthenticationBox: UserIdentifier { public init( authentication: MastodonAuthentication, domain: String, - userID: MastodonUser.ID, + userID: String, appAuthorization: Mastodon.API.OAuth.Authorization, userAuthorization: Mastodon.API.OAuth.Authorization, inMemoryCache: MastodonAccountInMemoryCache diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift index 4d125bd52..30d95a810 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift @@ -14,7 +14,7 @@ extension Notification.Property { public init( entity: Mastodon.Entity.Notification, domain: String, - userID: MastodonUser.ID, + userID: String, networkDate: Date ) { self.init( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index c1650e9b5..428ef6eec 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -14,8 +14,8 @@ import MastodonSDK extension APIService { private struct MastodonBlockContext { - let sourceUserID: MastodonUser.ID - let targetUserID: MastodonUser.ID + let sourceUserID: String + let targetUserID: String let targetUsername: String let isBlocking: Bool let isFollowing: Bool diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index dadb34e29..398eaf2ee 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -14,8 +14,8 @@ import MastodonSDK extension APIService { private struct MastodonFollowContext { - let sourceUserID: MastodonUser.ID - let targetUserID: MastodonUser.ID + let sourceUserID: String + let targetUserID: String let isFollowing: Bool let isPending: Bool let needsUnfollow: Bool diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index d45b33e2e..b97e405b9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -14,7 +14,7 @@ import MastodonSDK extension APIService { private struct MastodonMuteContext { - let targetUserID: MastodonUser.ID + let targetUserID: String let targetUsername: String let isMuting: Bool } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index 2df898977..b6c5ebcd4 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -20,7 +20,7 @@ extension APIService { let managedObjectContext = backgroundManagedObjectContext let _query: Mastodon.API.Account.RelationshipQuery? = try? await managedObjectContext.perform { - var ids: [MastodonUser.ID] = [] + var ids: [String] = [] for record in records { guard let user = record.object(in: managedObjectContext) else { continue } guard user.id != authenticationBox.userID else { continue } @@ -71,7 +71,7 @@ extension APIService { authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { - let ids: [MastodonUser.ID] = accounts.compactMap { $0.id } + let ids: [String] = accounts.compactMap { $0.id } guard ids.isEmpty == false else { throw APIError.implicit(.badRequest) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index e43b812c9..d2a0b2ed1 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -125,7 +125,7 @@ public final class AuthenticationService: NSObject { extension AuthenticationService { - public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool { + public func activeMastodonUser(domain: String, userID: String) async throws -> Bool { var isActive = false AuthenticationServiceProvider.shared.activateAuthentication(in: domain, for: userID) From 3abb80a5df11f613f4443951369af9eb5c9e8b64 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 14 Dec 2023 13:48:59 +0100 Subject: [PATCH 059/159] Migrate UserList to use Accounts (IOS-192) --- .../Follower/FollowerListViewModel.swift | 2 - ...dByViewController+DataSourceProvider.swift | 8 +-- ...dByViewController+DataSourceProvider.swift | 10 ++-- .../UserLIst/UserListViewModel+Diffable.swift | 15 ++++- .../UserLIst/UserListViewModel+State.swift | 59 +++++++++++++------ .../Profile/UserLIst/UserListViewModel.swift | 11 ++-- 6 files changed, 66 insertions(+), 39 deletions(-) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index b0d31417a..0f4b2fba1 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -7,8 +7,6 @@ import Foundation import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonSDK import MastodonCore diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift index 4f9580fbd..57e0c1408 100644 --- a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift @@ -21,10 +21,10 @@ extension FavoritedByViewController: DataSourceProvider { } switch item { - case .user(let record): - return .user(record: record) - default: - return nil + case .user(_), .bottomHeader(_), .bottomLoader: + return nil + case .account(let account, let relationship): + return .account(account: account, relationship: relationship) } } diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift index 61387c044..8332aa87a 100644 --- a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift @@ -20,12 +20,12 @@ extension RebloggedByViewController: DataSourceProvider { guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return nil } - + switch item { - case .user(let record): - return .user(record: record) - default: - return nil + case .user(_), .bottomHeader(_), .bottomLoader: + return nil + case .account(let account, let relationship): + return .account(account: account, relationship: relationship) } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift index d4830affc..b66f77785 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift @@ -9,6 +9,7 @@ import UIKit import MastodonAsset import MastodonLocalization import Combine +import MastodonSDK extension UserListViewModel { @MainActor @@ -33,15 +34,23 @@ extension UserListViewModel { // trigger initial loading stateMachine.enter(UserListViewModel.State.Reloading.self) - userFetchedResultsController.$records + $accounts .receive(on: DispatchQueue.main) - .sink { [weak self] records in + .sink { [weak self] accounts in guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) - let items = records.map { UserItem.user(record: $0) } + + let accountsWithRelationship: [(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)] = accounts.compactMap { account in + guard let relationship = self.relationships.first(where: {$0.id == account.id }) else { return (account: account, relationship: nil)} + + return (account: account, relationship: relationship) + } + + let items = accountsWithRelationship.map { UserItem.account(account: $0.account, relationship: $0.relationship) } snapshot.appendItems(items, toSection: .main) if let currentState = self.stateMachine.currentState { diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift index cb6e9d3fa..912dcd7e3 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift @@ -30,7 +30,7 @@ extension UserListViewModel { extension UserListViewModel.State { class Initial: UserListViewModel.State { override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let _ = viewModel else { return false } + guard viewModel != nil else { return false } switch stateClass { case is Reloading.Type: return true @@ -52,10 +52,10 @@ extension UserListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel, let stateMachine else { return } // reset - viewModel.userFetchedResultsController.userIDs = [] + viewModel.accounts = [] stateMachine.enter(Loading.self) } @@ -74,8 +74,8 @@ extension UserListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let _ = viewModel, let stateMachine = stateMachine else { return } - + guard viewModel != nil, let stateMachine else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { stateMachine.enter(Loading.self) } @@ -117,10 +117,11 @@ extension UserListViewModel.State { maxID = nil } - guard let viewModel = viewModel else { return } + guard let viewModel else { return } let maxID = self.maxID - + let authenticationBox = viewModel.authContext.mastodonAuthenticationBox + Task { do { let response: Mastodon.Response.Content<[Mastodon.Entity.Account]> @@ -129,24 +130,45 @@ extension UserListViewModel.State { response = try await viewModel.context.apiService.favoritedBy( status: status, query: .init(maxID: maxID, limit: nil), - authenticationBox: viewModel.authContext.mastodonAuthenticationBox + authenticationBox: authenticationBox ) case .rebloggedBy(let status): response = try await viewModel.context.apiService.rebloggedBy( status: status, query: .init(maxID: maxID, limit: nil), - authenticationBox: viewModel.authContext.mastodonAuthenticationBox + authenticationBox: authenticationBox ) } + if response.value.isEmpty { + await enter(state: NoMore.self) + + viewModel.accounts = [] + viewModel.relationships = [] + return + } + + let newRelationships = try await viewModel.context.apiService.relationship( + forAccounts: response.value, + authenticationBox: authenticationBox + ) + var hasNewAppend = false - var userIDs = viewModel.userFetchedResultsController.userIDs - for user in response.value { - guard !userIDs.contains(user.id) else { continue } - userIDs.append(user.id) + var accounts = viewModel.accounts + for account in response.value { + guard !accounts.contains(account) else { continue } + + accounts.append(account) hasNewAppend = true } - + + var relationships = viewModel.relationships + + for relationship in newRelationships.value { + guard relationships.contains(relationship) == false else { continue } + relationships.append(relationship) + } + let maxID = response.link?.maxID if hasNewAppend, maxID != nil { @@ -155,8 +177,9 @@ extension UserListViewModel.State { await enter(state: NoMore.self) } self.maxID = maxID - viewModel.userFetchedResultsController.userIDs = userIDs - + viewModel.relationships = relationships + viewModel.accounts = accounts + } catch { await enter(state: Fail.self) } @@ -177,9 +200,9 @@ extension UserListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel else { return } + guard let viewModel else { return } // trigger reload - viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs + viewModel.accounts = viewModel.accounts } } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift index 9e26b81e6..14665334a 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift @@ -19,7 +19,8 @@ final class UserListViewModel { let context: AppContext let authContext: AuthContext let kind: Kind - let userFetchedResultsController: UserFetchedResultsController + @Published var accounts: [Mastodon.Entity.Account] + @Published var relationships: [Mastodon.Entity.Relationship] let listBatchFetchViewModel = ListBatchFetchViewModel() // output @@ -44,12 +45,8 @@ final class UserListViewModel { self.context = context self.authContext = authContext self.kind = kind - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalPredicate: nil - ) - // end init + self.accounts = [] + self.relationships = [] } } From f373506aa3d234331f168b1dce1422043958cfba Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 14 Dec 2023 14:07:42 +0100 Subject: [PATCH 060/159] Remove user from UserItem-lists (IOS-192) Also: Remove CoreData and replace User with Account in several places --- Mastodon/Diffable/User/UserItem.swift | 1 - Mastodon/Diffable/User/UserSection.swift | 43 +-------- .../Provider/DataSourceFacade+Block.swift | 4 +- .../Provider/DataSourceFacade+Follow.swift | 38 ++------ .../Provider/DataSourceFacade+Profile.swift | 16 +--- .../Provider/DataSourceFacade+Status.swift | 2 +- .../Provider/DataSourceFacade+UserView.swift | 81 +++------------- .../DiscoveryForYouViewController.swift | 6 +- ...dByViewController+DataSourceProvider.swift | 2 +- ...dByViewController+DataSourceProvider.swift | 2 +- .../ReportResultViewController.swift | 2 +- .../SearchResult/SearchResultSection.swift | 19 +--- .../View/Content/UserView+Configuration.swift | 52 ----------- .../UserTableViewCell+ViewModel.swift | 67 ++------------ .../SuggestionAccountViewModel.swift | 2 +- .../Service/API/APIService+Block.swift | 71 +++----------- .../Service/API/APIService+Follow.swift | 92 +------------------ .../Service/API/APIService+Follower.swift | 23 +---- .../View/Content/UserView+ViewModel.swift | 1 - .../MastodonUI/View/Content/UserView.swift | 7 +- 20 files changed, 58 insertions(+), 473 deletions(-) diff --git a/Mastodon/Diffable/User/UserItem.swift b/Mastodon/Diffable/User/UserItem.swift index ba44aa52a..51c9cb443 100644 --- a/Mastodon/Diffable/User/UserItem.swift +++ b/Mastodon/Diffable/User/UserItem.swift @@ -11,7 +11,6 @@ import CoreDataStack import MastodonSDK enum UserItem: Hashable { - case user(record: ManagedObjectRecord) case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) case bottomLoader case bottomHeader(text: String) diff --git a/Mastodon/Diffable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift index 6997e5159..1483de2e4 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -37,7 +37,7 @@ extension UserSection { case .account(let account, let relationship): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell - guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell } + guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return cell } cell.userView.setButtonState(.loading) cell.configure( @@ -48,27 +48,6 @@ extension UserSection { delegate: userTableViewCellDelegate ) - return cell - - case .user(let record): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - configure( - context: context, - authContext: authContext, - tableView: tableView, - cell: cell, - viewModel: UserTableViewCell.ViewModel( - user: user, - followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), - blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), - followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() - ), - userTableViewCellDelegate: userTableViewCellDelegate - ) - } - return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell @@ -82,23 +61,3 @@ extension UserSection { } } } - -extension UserSection { - - static func configure( - context: AppContext, - authContext: AuthContext, - tableView: UITableView, - cell: UserTableViewCell, - viewModel: UserTableViewCell.ViewModel, - userTableViewCellDelegate: UserTableViewCellDelegate? - ) { - cell.configure( - me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), - tableView: tableView, - viewModel: viewModel, - delegate: userTableViewCellDelegate - ) - } - -} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index c8f1f9405..c87217595 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -13,7 +13,7 @@ import MastodonSDK extension DataSourceFacade { static func responseToUserBlockAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + account: Mastodon.Entity.Account ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() @@ -22,7 +22,7 @@ extension DataSourceFacade { let authBox = dependency.authContext.mastodonAuthenticationBox _ = try await apiService.toggleBlock( - user: user, + account: account, authenticationBox: authBox ) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 84deba2ca..134e00ccb 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -15,27 +15,13 @@ import MastodonLocalization extension DataSourceFacade { static func responseToUserFollowAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord - ) async throws { - let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() - await selectionFeedbackGenerator.selectionChanged() - - _ = try await dependency.context.apiService.toggleFollow( - user: user, - authenticationBox: dependency.authContext.mastodonAuthenticationBox - ) - dependency.context.authenticationService.fetchFollowingAndBlockedAsync() - } - - static func responseToUserFollowAction( - dependency: NeedsDependency & AuthContextProvider, - user: Mastodon.Entity.Account + account: Mastodon.Entity.Account ) async throws -> Mastodon.Entity.Relationship { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() let response = try await dependency.context.apiService.toggleFollow( - user: user, + account: account, authenticationBox: dependency.authContext.mastodonAuthenticationBox ).value @@ -118,22 +104,12 @@ extension DataSourceFacade { } extension DataSourceFacade { - static func responseToShowHideReblogAction( - dependency: NeedsDependency & AuthContextProvider, - account: Mastodon.Entity.Account - ) async throws { - #warning("TODO: Implement") -// _ = try await dependency.context.apiService.toggleShowReblogs( -// for: user, -// authenticationBox: dependency.authContext.mastodonAuthenticationBox) - } - static func responseToShowHideReblogAction( - dependency: NeedsDependency & AuthContextProvider, - user: Mastodon.Entity.Account + dependency: NeedsDependency & AuthContextProvider, + account: Mastodon.Entity.Account ) async throws { - _ = try await dependency.context.apiService.toggleShowReblogs( - for: user, - authenticationBox: dependency.authContext.mastodonAuthenticationBox) + _ = try await dependency.context.apiService.toggleShowReblogs( + for: account, + authenticationBox: dependency.authContext.mastodonAuthenticationBox) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 464ecd374..30f556a2c 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -144,20 +144,8 @@ extension DataSourceFacade { return } - -#warning("TODO: Implement") - await DataSourceFacade.coordinateToProfileScene(provider: provider, domain: "", accountID: mention.id) -// let profileViewModel = ProfileViewModel( -// context: provider.context, -// authContext: provider.authContext, -// account: status.entity.account -// ) -// -// _ = provider.coordinator.present( -// scene: .profile(viewModel: profileViewModel), -// from: provider, -// transition: .show -// ) + + await DataSourceFacade.coordinateToProfileScene(provider: provider, domain: domain, accountID: mention.id) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 375c48575..9147569a7 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -354,7 +354,7 @@ extension DataSourceFacade { break case .followUser(_): try await DataSourceFacade.responseToUserFollowAction(dependency: dependency, - user: menuContext.author) + account: menuContext.author) } } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift index a34c2ad4a..8cea808fa 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift @@ -9,105 +9,46 @@ import MastodonSDK extension DataSourceFacade { static func responseToUserViewButtonAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord, - buttonState: UserView.ButtonState - ) async throws { - switch buttonState { - case .follow: - try await DataSourceFacade.responseToUserFollowAction( - dependency: dependency, - user: user - ) - - if let userObject = user.object(in: dependency.context.managedObjectContext) { - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(userObject.id) - } - - case .request: - try await DataSourceFacade.responseToUserFollowAction( - dependency: dependency, - user: user - ) - - if let userObject = user.object(in: dependency.context.managedObjectContext) { - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(userObject.id) - } - - case .unfollow: - try await DataSourceFacade.responseToUserFollowAction( - dependency: dependency, - user: user - ) - if let userObject = user.object(in: dependency.context.managedObjectContext) { - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == userObject.id }) - } - case .blocked: - try await DataSourceFacade.responseToUserBlockAction( - dependency: dependency, - user: user - ) - - if let userObject = user.object(in: dependency.context.managedObjectContext) { - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(userObject.id) - } - - case .pending: - try await DataSourceFacade.responseToUserFollowAction( - dependency: dependency, - user: user - ) - - if let userObject = user.object(in: dependency.context.managedObjectContext) { - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == userObject.id }) - } - case .none, .loading: - break //no-op - } - } - - static func responseToUserViewButtonAction( - dependency: NeedsDependency & AuthContextProvider, - user: Mastodon.Entity.Account, + account: Mastodon.Entity.Account, buttonState: UserView.ButtonState ) async throws { switch buttonState { case .follow: _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, - user: user + account: account ) - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(user.id) - + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(account.id) case .request: _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, - user: user + account: account ) - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(user.id) + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(account.id) case .unfollow: _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, - user: user + account: account ) - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == user.id }) + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == account.id }) case .blocked: try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, - user: user + account: account ) - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(user.id) + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(account.id) case .pending: _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, - user: user + account: account ) - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == user.id }) + dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == account.id }) case .none, .loading: break //no-op } diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift index 90a136611..1de2dd704 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift @@ -99,9 +99,7 @@ extension DiscoveryForYouViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard case let .account(account, _) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } - Task { - await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) - } + DataSourceFacade.coordinateToProfileScene(provider: self, account: account) } } @@ -119,7 +117,7 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate { cell.profileCardView.setButtonState(.loading) Task { - let newRelationship = try await DataSourceFacade.responseToUserFollowAction(dependency: self, user: account) + let newRelationship = try await DataSourceFacade.responseToUserFollowAction(dependency: self, account: account) let isMe = (account.id == authContext.mastodonAuthenticationBox.userID) diff --git a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift index 57e0c1408..53ccc6638 100644 --- a/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/FavoritedBy/FavoritedByViewController+DataSourceProvider.swift @@ -21,7 +21,7 @@ extension FavoritedByViewController: DataSourceProvider { } switch item { - case .user(_), .bottomHeader(_), .bottomLoader: + case .bottomHeader(_), .bottomLoader: return nil case .account(let account, let relationship): return .account(account: account, relationship: relationship) diff --git a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift index 8332aa87a..57833a1b1 100644 --- a/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Profile/UserLIst/RebloggedBy/RebloggedByViewController+DataSourceProvider.swift @@ -22,7 +22,7 @@ extension RebloggedByViewController: DataSourceProvider { } switch item { - case .user(_), .bottomHeader(_), .bottomLoader: + case .bottomHeader(_), .bottomLoader: return nil case .account(let account, let relationship): return .account(account: account, relationship: relationship) diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index 0b9ab1fba..5a6ec366f 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -90,7 +90,7 @@ extension ReportResultViewController { do { try await DataSourceFacade.responseToUserFollowAction( dependency: self, - user: self.viewModel.account + account: self.viewModel.account ) } catch { // handle error diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift index 42b1a1585..254d301ff 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultSection.swift @@ -44,7 +44,7 @@ extension SearchResultSection { case .account(let account, let relationship): let cell = tableView.dequeueReusableCell(withIdentifier: UserTableViewCell.reuseIdentifier, for: indexPath) as! UserTableViewCell - guard let me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { return cell } + guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return cell } cell.userView.setButtonState(.loading) cell.configure( @@ -110,21 +110,4 @@ extension SearchResultSection { delegate: configuration.statusViewTableViewCellDelegate ) } - - static func configure( - context: AppContext, - authContext: AuthContext, - tableView: UITableView, - cell: UserTableViewCell, - viewModel: UserTableViewCell.ViewModel, - configuration: Configuration - ) { - cell.configure( - me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), - tableView: tableView, - viewModel: viewModel, - delegate: configuration.userTableViewCellDelegate - ) - } - } diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index e3f91f462..4a3595c73 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -17,61 +17,9 @@ import MastodonSDK import MastodonAsset extension UserView { - public func configure(user: MastodonUser, delegate: UserViewDelegate?) { - self.delegate = delegate - viewModel.user = user - viewModel.account = nil - viewModel.relationship = nil - - Publishers.CombineLatest( - user.publisher(for: \.avatar), - UserDefaults.shared.publisher(for: \.preferredStaticAvatar) - ) - .map { _ in user.avatarImageURL() } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) - - // author name - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _, emojis in - do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure(error.localizedDescription) - return PlaintextMetaContent(string: user.displayNameWithFallback) - } - } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) - // author username - user.publisher(for: \.acct) - .map { $0 as String? } - .assign(to: \.authorUsername, on: viewModel) - .store(in: &disposeBag) - - user.publisher(for: \.followersCount) - .map { Int($0) } - .assign(to: \.authorFollowers, on: viewModel) - .store(in: &disposeBag) - - user.publisher(for: \.fields) - .map { fields in - let firstVerified = fields.first(where: { $0.verifiedAt != nil }) - return firstVerified?.value - } - .assign(to: \.authorVerifiedLink, on: viewModel) - .store(in: &disposeBag) - } - func configure(with account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, delegate: UserViewDelegate?) { viewModel.account = account viewModel.relationship = relationship - viewModel.user = nil self.delegate = delegate let authorUsername = PlaintextMetaContent(string: "@\(account.username)") diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 6b8613292..ffb927166 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -14,14 +14,14 @@ import MastodonSDK extension UserTableViewCell { final class ViewModel { - let user: MastodonUser - + let account: Mastodon.Entity.Account + let followedUsers: AnyPublisher<[String], Never> let blockedUsers: AnyPublisher<[String], Never> let followRequestedUsers: AnyPublisher<[String], Never> - init(user: MastodonUser, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { - self.user = user + init(account: Mastodon.Entity.Account, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { + self.account = account self.followedUsers = followedUsers self.followRequestedUsers = followRequestedUsers self.blockedUsers = blockedUsers @@ -32,7 +32,7 @@ extension UserTableViewCell { extension UserTableViewCell { func configure( - me: MastodonUser, + me: Mastodon.Entity.Account, tableView: UITableView, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, @@ -45,69 +45,16 @@ extension UserTableViewCell { self.delegate = delegate } - - func configure( - me: MastodonUser? = nil, - tableView: UITableView, - viewModel: ViewModel, - delegate: UserTableViewCellDelegate? - ) { - userView.configure(user: viewModel.user, delegate: delegate) - - guard let me = me else { - return userView.setButtonState(.none) - } - - if viewModel.user == me { - userView.setButtonState(.none) - } else { - userView.setButtonState(.loading) - } - - Publishers.CombineLatest3( - viewModel.followedUsers, - viewModel.followRequestedUsers, - viewModel.blockedUsers - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] followed, requested, blocked in - if viewModel.user == me { - self?.userView.setButtonState(.none) - } else if blocked.contains(viewModel.user.id) { - self?.userView.setButtonState(.blocked) - } else if followed.contains(viewModel.user.id) { - self?.userView.setButtonState(.unfollow) - } else if requested.contains(viewModel.user.id) { - self?.userView.setButtonState(.pending) - } else if viewModel.user.locked { - self?.userView.setButtonState(.request) - } else if viewModel.user != me { - self?.userView.setButtonState(.follow) - } - } - .store(in: &disposeBag) - - self.delegate = delegate - } } extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextProvider { - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) { - Task { - try await DataSourceFacade.responseToUserViewButtonAction( - dependency: self, - user: user.asRecord, - buttonState: state - ) - } - } - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: MastodonUser?) { + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for account: Mastodon.Entity.Account, me: Mastodon.Entity.Account?) { Task { await MainActor.run { view.setButtonState(.loading) } try await DataSourceFacade.responseToUserViewButtonAction( dependency: self, - user: account, + account: account, buttonState: state ) diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift index 84cc4c46d..389da8427 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift @@ -121,7 +121,7 @@ final class SuggestionAccountViewModel: NSObject { taskGroup.addTask { try? await DataSourceFacade.responseToUserViewButtonAction( dependency: dependency, - user: account, + account: account, buttonState: .follow ) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 428ef6eec..8d70ead23 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -61,40 +61,22 @@ extension APIService { } public func toggleBlock( - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - - let managedObjectContext = backgroundManagedObjectContext - let blockContext: MastodonBlockContext = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let user = user.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { - throw APIError.implicit(.badRequest) - } - let isBlocking = user.blockingBy.contains(me) - let isFollowing = user.followingBy.contains(me) - // toggle block state - user.update(isBlocking: !isBlocking, by: me) - // update follow state implicitly - if !isBlocking { - // will do block action. set to unfollow - user.update(isFollowing: false, by: me) - } + guard let me = authenticationBox.authentication.account(), + let relationship = try await relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first + else { throw APIError.implicit(.badRequest) } + + let blockContext = MastodonBlockContext( + sourceUserID: me.id, + targetUserID: account.id, + targetUsername: account.username, + isBlocking: relationship.blocking, + isFollowing: relationship.following + ) - return MastodonBlockContext( - sourceUserID: me.id, - targetUserID: user.id, - targetUsername: user.username, - isBlocking: isBlocking, - isFollowing: isFollowing - ) - } - let result: Result, Error> do { if blockContext.isBlocking { @@ -117,34 +99,7 @@ extension APIService { } catch { result = .failure(error) } - - try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication - - guard - let user = user.object(in: managedObjectContext), - let me = authentication.user(in: managedObjectContext) - else { return } - - - switch result { - case .success(let response): - let relationship = response.value - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: relationship, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isBlocking: blockContext.isBlocking, by: me) - user.update(isFollowing: blockContext.isFollowing, by: me) - } - } - + let response = try result.get() return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 398eaf2ee..c27e94bc4 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -30,95 +30,11 @@ extension APIService { /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox` /// - Returns: publisher for `Relationship` public func toggleFollow( - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext - let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return nil } - guard let user = user.object(in: managedObjectContext) else { return nil } - - let isFollowing = user.followingBy.contains(me) - let isPending = user.followRequestedBy.contains(me) - let needsUnfollow = isFollowing || isPending - - if needsUnfollow { - // unfollow - user.update(isFollowing: false, by: me) - user.update(isFollowRequested: false, by: me) - } else { - // follow - if user.locked { - user.update(isFollowing: false, by: me) - user.update(isFollowRequested: true, by: me) - } else { - user.update(isFollowing: true, by: me) - user.update(isFollowRequested: false, by: me) - } - } - let context = MastodonFollowContext( - sourceUserID: me.id, - targetUserID: user.id, - isFollowing: isFollowing, - isPending: isPending, - needsUnfollow: needsUnfollow - ) - return context - } - - guard let followContext = _followContext else { - throw APIError.implicit(.badRequest) - } - - // request follow or unfollow - let result: Result, Error> - do { - let response = try await Mastodon.API.Account.follow( - session: session, - domain: authenticationBox.domain, - accountID: followContext.targetUserID, - followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), - authorization: authenticationBox.userAuthorization - ).singleOutput() - result = .success(response) - } catch { - result = .failure(error) - } - - // update friendship state - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext), - let user = user.object(in: managedObjectContext) - else { return } - - switch result { - case .success(let response): - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - case .failure: - // rollback - user.update(isFollowing: followContext.isFollowing, by: me) - user.update(isFollowRequested: followContext.isPending, by: me) - } - } - - let response = try result.get() - return response - } - - public func toggleFollow( - user: Mastodon.Entity.Account, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - - guard let relationship = try await relationship(forAccounts: [user], authenticationBox: authenticationBox).value.first else { + guard let relationship = try await relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first else { throw APIError.implicit(.badRequest) } @@ -129,14 +45,14 @@ extension APIService { response = try await Mastodon.API.Account.unfollow( session: session, domain: authenticationBox.domain, - accountID: user.id, + accountID: account.id, authorization: authenticationBox.userAuthorization ).singleOutput() } else { response = try await Mastodon.API.Account.follow( session: session, domain: authenticationBox.domain, - accountID: user.id, + accountID: account.id, followQueryType: .follow(query: .init()), authorization: authenticationBox.userAuthorization ).singleOutput() diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift index f463501f6..b55287cd3 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follower.swift @@ -32,28 +32,7 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - - for entity in response.value { - let result = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - - let user = result.user - me?.update(isFollowing: true, by: user) - } - } - + return response } - } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index f35afb97b..c29bdd88d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -26,7 +26,6 @@ extension UserView { @Published public var authorUsername: String? @Published public var authorFollowers: Int? @Published public var authorVerifiedLink: String? - @Published public var user: MastodonUser? @Published public var account: Mastodon.Entity.Account? @Published public var relationship: Mastodon.Entity.Relationship? } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index 6ad31bc75..b58819df5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -15,8 +15,7 @@ import CoreDataStack import MastodonSDK public protocol UserViewDelegate: AnyObject { - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: MastodonUser) - func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account, me: MastodonUser?) + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account, me: Mastodon.Entity.Account?) } public final class UserView: UIView { @@ -255,9 +254,7 @@ public extension UserView { } @objc private func didTapFollowButton() { - if let user = viewModel.user { - delegate?.userView(self, didTapButtonWith: currentButtonState, for: user) - } else if let account = viewModel.account { + if let account = viewModel.account { delegate?.userView(self, didTapButtonWith: currentButtonState, for: account, me: nil) } } From 6cc069ec5bf899da43f9ed6882d640a3661f4713 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 14 Dec 2023 14:07:58 +0100 Subject: [PATCH 061/159] Try to fix domain (IOS-192) --- Mastodon/Scene/Profile/ProfileViewController.swift | 10 +++++----- .../MastodonSDK/Entity/Mastodon+Entity+Account.swift | 11 +++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index b43694483..f2fc6885a 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -298,9 +298,9 @@ extension ProfileViewController { // header #warning("TODO: Implement") let headerViewModel = profileHeaderViewController.viewModel! -// viewModel.$account -// .assign(to: \.account, on: headerViewModel) -// .store(in: &disposeBag) + viewModel.$account + .assign(to: \.account, on: headerViewModel) + .store(in: &disposeBag) viewModel.$isEditing .assign(to: \.isEditing, on: headerViewModel) .store(in: &disposeBag) @@ -378,11 +378,11 @@ extension ProfileViewController { profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear ) .sink { [weak self] (user, _) in - guard let self else { return } + guard let self, let domain = user.domainFromAcct else { return } Task { _ = try await self.context.apiService.fetchUser( username: user.username, - domain: "user.domainFromAcct", + domain: domain, authenticationBox: self.authContext.mastodonAuthenticationBox ) } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index a358bcace..4de477896 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -178,4 +178,15 @@ extension Mastodon.Entity.Account { return !displayName.isEmpty ? displayName : username } + + public var domainFromAcct: String? { + if acct.contains("@") { + return domain + } else if let domain = acct.split(separator: "@").last { + return String(domain) + } else { + return nil + } + } + } From 16250b440d681596ef487de8a5526f440f582d74 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 14 Dec 2023 15:56:32 +0100 Subject: [PATCH 062/159] Fix domain :facepalm: (IOS-192) --- .../Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 4de477896..836160db7 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -180,7 +180,7 @@ extension Mastodon.Entity.Account { } public var domainFromAcct: String? { - if acct.contains("@") { + if acct.contains("@") == false { return domain } else if let domain = acct.split(separator: "@").last { return String(domain) From 6aea178609545335bb7aee246210565b25f456b8 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 14 Dec 2023 16:14:16 +0100 Subject: [PATCH 063/159] Add relationship to ProfileViewModel (IOS-192) --- Mastodon/Coordinator/SceneCoordinator.swift | 7 ++- .../Provider/DataSourceFacade+Profile.swift | 28 ++++++---- .../NotificationTimelineViewController.swift | 11 +--- Mastodon/Scene/Profile/ProfileViewModel.swift | 4 +- .../Root/MainTab/MainTabBarController.swift | 2 +- .../AboutInstanceViewController.swift | 14 ++++- .../ServerDetailsViewController.swift | 5 +- .../Scene/Settings/SettingsCoordinator.swift | 5 +- Mastodon/Supporting Files/SceneDelegate.swift | 17 +++++- .../Service/API/APIService+Relationship.swift | 54 ------------------- .../Service/API/APIService+Search.swift | 19 ------- 11 files changed, 59 insertions(+), 107 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 50f5dfb0f..1b9e99521 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -107,11 +107,14 @@ final public class SceneCoordinator { notificationID: notificationID, authenticationBox: authContext.mastodonAuthenticationBox ).value.account - + + let relationship = try await appContext.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first + let profileViewModel = ProfileViewModel( context: appContext, authContext: authContext, - account: account + account: account, + relationship: relationship ) _ = self.present( scene: .profile(viewModel: profileViewModel), diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 30f556a2c..24e059f1c 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -102,17 +102,23 @@ extension DataSourceFacade { account: Mastodon.Entity.Account ) { - let profileViewModel = ProfileViewModel( - context: provider.context, - authContext: provider.authContext, - account: account - ) - - _ = provider.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: provider, - transition: .show - ) + Task { @MainActor in + + guard let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else { return } + + let profileViewModel = ProfileViewModel( + context: provider.context, + authContext: provider.authContext, + account: account, + relationship: relationship + ) + + _ = provider.coordinator.present( + scene: .profile(viewModel: profileViewModel), + from: provider, + transition: .show + ) + } } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 0ebd0511b..05d290742 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -296,16 +296,7 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { ) } else { - let profileViewModel = ProfileViewModel( - context: self.context, - authContext: self.viewModel.authContext, - account: notification.account - ) - _ = self.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: self, - transition: .show - ) + DataSourceFacade.coordinateToProfileScene(provider: self, account: notification.account) } default: break diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 01b29a46f..7c705ecc8 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -56,10 +56,12 @@ class ProfileViewModel: NSObject { // let needsPagePinToTop = CurrentValueSubject(false) @MainActor - init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account) { + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) { self.context = context self.authContext = authContext self.account = account + self.relationship = relationship + self.postsUserTimelineViewModel = UserTimelineViewModel( context: context, authContext: authContext, diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 258585c1d..f247b83cc 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -119,7 +119,7 @@ class MainTabBarController: UITabBarController { let _viewController = ProfileViewController() _viewController.context = context _viewController.coordinator = coordinator - _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me) + _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil) viewController = _viewController } viewController.title = self.title diff --git a/Mastodon/Scene/Settings/Server Details/AboutInstanceViewController.swift b/Mastodon/Scene/Settings/Server Details/AboutInstanceViewController.swift index 59c1d5b01..e41dde9d0 100644 --- a/Mastodon/Scene/Settings/Server Details/AboutInstanceViewController.swift +++ b/Mastodon/Scene/Settings/Server Details/AboutInstanceViewController.swift @@ -2,13 +2,18 @@ import UIKit import MastodonSDK +import MastodonCore protocol AboutInstanceViewControllerDelegate: AnyObject { func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) func sendEmailToAdmin(_ viewController: AboutInstanceViewController, emailAddress: String) } -class AboutInstanceViewController: UIViewController { +class AboutInstanceViewController: UIViewController, NeedsDependency, AuthContextProvider { + + var authContext: AuthContext + var context: AppContext! + var coordinator: SceneCoordinator! weak var delegate: AboutInstanceViewControllerDelegate? var dataSource: AboutInstanceTableViewDataSource? @@ -19,7 +24,12 @@ class AboutInstanceViewController: UIViewController { var instance: Mastodon.Entity.V2.Instance? - init() { + init(context: AppContext, authContext: AuthContext, coordinator: SceneCoordinator) { + + self.context = context + self.authContext = authContext + self.coordinator = coordinator + tableView = UITableView(frame: .zero, style: .insetGrouped) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.register(ContactAdminTableViewCell.self, forCellReuseIdentifier: ContactAdminTableViewCell.reuseIdentifier) diff --git a/Mastodon/Scene/Settings/Server Details/ServerDetailsViewController.swift b/Mastodon/Scene/Settings/Server Details/ServerDetailsViewController.swift index fb724f8f9..4dd19b7c0 100644 --- a/Mastodon/Scene/Settings/Server Details/ServerDetailsViewController.swift +++ b/Mastodon/Scene/Settings/Server Details/ServerDetailsViewController.swift @@ -4,6 +4,7 @@ import UIKit import MastodonSDK import MastodonLocalization import MetaTextKit +import MastodonCore enum ServerDetailsTab: Int, CaseIterable { case about = 0 @@ -36,7 +37,7 @@ class ServerDetailsViewController: UIViewController { let instanceRulesViewController: InstanceRulesViewController let containerView: UIView - init(domain: String) { + init(domain: String, appContext: AppContext, authContext: AuthContext, sceneCoordinator: SceneCoordinator) { segmentedControl = UISegmentedControl() segmentedControl.translatesAutoresizingMaskIntoConstraints = false @@ -47,7 +48,7 @@ class ServerDetailsViewController: UIViewController { containerView = UIView() containerView.translatesAutoresizingMaskIntoConstraints = false - aboutInstanceViewController = AboutInstanceViewController() + aboutInstanceViewController = AboutInstanceViewController(context: appContext, authContext: authContext, coordinator: sceneCoordinator) instanceRulesViewController = InstanceRulesViewController() pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index b1150e3f7..8d9e40695 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -70,7 +70,7 @@ extension SettingsCoordinator: SettingsViewControllerDelegate { navigationController.pushViewController(notificationViewController, animated: true) case .serverDetails(let domain): - let serverDetailsViewController = ServerDetailsViewController(domain: domain) + let serverDetailsViewController = ServerDetailsViewController(domain: domain, appContext: appContext, authContext: authContext, sceneCoordinator: sceneCoordinator) serverDetailsViewController.delegate = self appContext.apiService.instanceV2(domain: domain) @@ -217,8 +217,7 @@ extension SettingsCoordinator: ServerDetailsViewControllerDelegate { extension SettingsCoordinator: AboutInstanceViewControllerDelegate { @MainActor func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) { - let profileViewModel = ProfileViewModel(context: appContext, authContext: authContext, account: account) - sceneCoordinator.present(scene: .profile(viewModel: profileViewModel), transition: .show) + DataSourceFacade.coordinateToProfileScene(provider: viewController, account: account) } func sendEmailToAdmin(_ viewController: AboutInstanceViewController, emailAddress: String) { diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 9ecbfe58a..3bff14995 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -146,10 +146,16 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { authenticationBox: authenticationBox ).value.accounts.first else { return } + guard let relationship = try await AppContext.shared.apiService.relationship( + forAccounts: [account], + authenticationBox: authenticationBox + ).value.first else { return } + let profileViewModel = ProfileViewModel( context: AppContext.shared, authContext: authContext, - account: account + account: account, + relationship: relationship ) _ = self.coordinator?.present( scene: .profile(viewModel: profileViewModel), @@ -284,11 +290,18 @@ extension SceneDelegate { authenticationBox: authenticationBox ).value.accounts.first else { return } + guard let relationship = try await AppContext.shared.apiService.relationship( + forAccounts: [account], + authenticationBox: authenticationBox + ).value.first else { return } + let profileViewModel = ProfileViewModel( context: AppContext.shared, authContext: authContext, - account: account + account: account, + relationship: relationship ) + self.coordinator?.present( scene: .profile(viewModel: profileViewModel), from: nil, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index b6c5ebcd4..ce366ad7e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -12,60 +12,6 @@ import CoreDataStack import MastodonSDK extension APIService { - - public func relationship( - records: [ManagedObjectRecord], - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { - let managedObjectContext = backgroundManagedObjectContext - - let _query: Mastodon.API.Account.RelationshipQuery? = try? await managedObjectContext.perform { - var ids: [String] = [] - for record in records { - guard let user = record.object(in: managedObjectContext) else { continue } - guard user.id != authenticationBox.userID else { continue } - ids.append(user.id) - } - guard !ids.isEmpty else { return nil } - return Mastodon.API.Account.RelationshipQuery(ids: ids) - } - guard let query = _query else { - throw APIError.implicit(.badRequest) - } - - let response = try await Mastodon.API.Account.relationships( - session: session, - domain: authenticationBox.domain, - query: query, - authorization: authenticationBox.userAuthorization - ).singleOutput() - - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { - // assertionFailure() - return - } - - let relationships = response.value - for record in records { - guard let user = record.object(in: managedObjectContext) else { continue } - guard let relationship = relationships.first(where: { $0.id == user.id }) else { continue } - - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: relationship, - me: me, - networkDate: response.networkDate - ) - ) - } // end for in - } - - return response - } - - public func relationship( forAccounts accounts: [Mastodon.Entity.Account], authenticationBox: MastodonAuthenticationBox diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift index 123c01d17..3b3624344 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift @@ -24,25 +24,6 @@ extension APIService { query: query, authorization: authorization ).singleOutput() - - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - - // user - for entity in response.value.accounts { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - } - - } // ent try await managedObjectContext.performChanges { … } return response } From e7c1edbbc385b63a66b66e7ba3c455b816abaad9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 19 Dec 2023 22:21:15 +0100 Subject: [PATCH 064/159] Persist user after login (IOS-192) --- Mastodon.xcodeproj/project.pbxproj | 8 -------- .../Onboarding/Login/MastodonLoginViewController.swift | 8 ++++---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 2abbab917..4c4c2db0c 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -1883,13 +1883,6 @@ path = Localization; sourceTree = ""; }; - D8AC98742B0F615E0045EC2B /* Persistence */ = { - isa = PBXGroup; - children = ( - ); - path = Persistence; - sourceTree = ""; - }; D8E5C347296DB896007E76A7 /* Edit History */ = { isa = PBXGroup; children = ( @@ -2175,7 +2168,6 @@ DB427DD425BAA00100D1B89D /* Mastodon */ = { isa = PBXGroup; children = ( - D8AC98742B0F615E0045EC2B /* Persistence */, DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB427DE325BAA00100D1B89D /* Info.plist */, 2D76319C25C151DE00929FB9 /* Diffable */, diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 1ca300c8f..97d26a968 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -142,10 +142,10 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { authenticationViewModel .authenticated - .asyncMap { domain, user -> Result in + .asyncMap { domain, user -> Result in do { let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) - return .success(result) + return .success(user) } catch { return .failure(error) } @@ -156,8 +156,8 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { switch result { case .failure(let error): assertionFailure(error.localizedDescription) - case .success(let isActived): - assert(isActived) + case .success(let account): + FileManager.default.store(account: account, forUserID: account.id) self.coordinator.setup() } } From d1891190143154399d5e23ba2d7686f365de88d3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 20 Dec 2023 21:38:55 +0100 Subject: [PATCH 065/159] Remove obsolete Core Data-controller --- .../FollowedTagsFetchedResultController.swift | 75 ------------------- 1 file changed, 75 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift deleted file mode 100644 index 4c3670283..000000000 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FollowedTagsFetchedResultController.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// FollowedTagsFetchedResultController.swift -// -// -// Created by Marcus Kida on 23.11.22. -// - -import UIKit -import Combine -import CoreData -import CoreDataStack -import MastodonSDK - -public final class FollowedTagsFetchedResultController: NSObject { - - var disposeBag = Set() - - let fetchedResultsController: NSFetchedResultsController - - // input - @Published public var domain: String? = nil - @Published public var user: MastodonUser? = nil - - // output - @Published public private(set) var records: [Tag] = [] - - public init(managedObjectContext: NSManagedObjectContext, domain: String, user: MastodonUser) { - self.domain = domain - self.fetchedResultsController = { - let fetchRequest = Tag.sortedFetchRequest - fetchRequest.predicate = Tag.predicate(domain: domain, following: true, by: user) - fetchRequest.sortDescriptors = Tag.defaultSortDescriptors - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - return controller - }() - super.init() - - fetchedResultsController.delegate = self - try? fetchedResultsController.performFetch() - - Publishers.CombineLatest( - self.$domain, - self.$user - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] domain, user in - guard let self = self, let domain = domain, let user = user else { return } - self.fetchedResultsController.fetchRequest.predicate = Tag.predicate(domain: domain, following: true, by: user) - do { - try self.fetchedResultsController.performFetch() - } catch { - assertionFailure(error.localizedDescription) - } - } - .store(in: &disposeBag) - } - -} - -// MARK: - NSFetchedResultsControllerDelegate -extension FollowedTagsFetchedResultController: NSFetchedResultsControllerDelegate { - public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - - let objects = fetchedResultsController.fetchedObjects ?? [] - self.records = objects - } -} From 099cb724d0afd5685df194f7ad713556aa3dcf67 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 24 Dec 2023 14:25:32 +0100 Subject: [PATCH 066/159] Configure barbutton-items on profile-screen (IOS-192) --- .../Scene/Profile/ProfileViewController.swift | 220 ++++++++++-------- Mastodon/Scene/Profile/ProfileViewModel.swift | 73 +++--- 2 files changed, 155 insertions(+), 138 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index f2fc6885a..6b6488f32 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -202,33 +202,38 @@ extension ProfileViewController { } .store(in: &disposeBag) - Publishers.CombineLatest4 ( - viewModel.account.suspended.publisher, + // build items + Publishers.CombineLatest4( + viewModel.$relationship, profileHeaderViewController.viewModel.$isTitleViewDisplaying, - editingAndUpdatingPublisher.eraseToAnyPublisher(), - barButtonItemHiddenPublisher.eraseToAnyPublisher() + editingAndUpdatingPublisher, + barButtonItemHiddenPublisher ) .receive(on: DispatchQueue.main) - .sink { [weak self] isSuspended, isTitleViewDisplaying, tuple1, tuple2 in - guard let self = self else { return } + .sink { [weak self] account, isTitleViewDisplaying, tuple1, tuple2 in + guard let self else { return } let (isEditing, _) = tuple1 let (isMeBarButtonItemsHidden, isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden) = tuple2 var items: [UIBarButtonItem] = [] defer { - self.navigationItem.rightBarButtonItems = !items.isEmpty ? items : nil + if items.isNotEmpty { + self.navigationItem.rightBarButtonItems = items + } else { + self.navigationItem.rightBarButtonItems = nil + } } - guard !isSuspended else { + if let suspended = self.viewModel.account.suspended, suspended == true { return } - guard !isEditing else { + guard isEditing == false else { items.append(self.cancelEditingBarButtonItem) return } - guard !isTitleViewDisplaying else { + guard isTitleViewDisplaying == false else { return } @@ -274,8 +279,6 @@ extension ProfileViewController { bindTitleView() bindMoreBarButtonItem() bindPager() - - viewModel.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { @@ -286,7 +289,8 @@ extension ProfileViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + viewModel.viewDidAppear.send() + setNeedsStatusBarAppearanceUpdate() } @@ -296,7 +300,6 @@ extension ProfileViewController { private func bindViewModel() { // header -#warning("TODO: Implement") let headerViewModel = profileHeaderViewController.viewModel! viewModel.$account .assign(to: \.account, on: headerViewModel) @@ -307,9 +310,6 @@ extension ProfileViewController { viewModel.$isUpdating .assign(to: \.isUpdating, on: headerViewModel) .store(in: &disposeBag) -// viewModel.relationshipViewModel.$isMyself -// .assign(to: \.isMyself, on: headerViewModel) -// .store(in: &disposeBag) viewModel.$relationship .assign(to: \.relationship, on: headerViewModel) .store(in: &disposeBag) @@ -330,9 +330,9 @@ extension ProfileViewController { // about let aboutViewModel = viewModel.profileAboutViewModel -// viewModel.$account -// .assign(to: \.account, on: aboutViewModel) -// .store(in: &disposeBag) + viewModel.$account + .assign(to: \.account, on: aboutViewModel) + .store(in: &disposeBag) viewModel.$isEditing .assign(to: \.isEditing, on: aboutViewModel) .store(in: &disposeBag) @@ -373,21 +373,32 @@ extension ProfileViewController { self.navigationItem.title = name } .store(in: &disposeBag) - Publishers.CombineLatest( - profileHeaderViewController.viewModel.$account, - profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear - ) - .sink { [weak self] (user, _) in - guard let self, let domain = user.domainFromAcct else { return } - Task { - _ = try await self.context.apiService.fetchUser( - username: user.username, - domain: domain, - authenticationBox: self.authContext.mastodonAuthenticationBox - ) - } - } - .store(in: &disposeBag) + + profileHeaderViewController.profileHeaderView.viewModel.viewDidAppear + .sink(receiveValue: { [weak self] _ in + + guard let self else { return } + let account = self.viewModel.account + guard let domain = account.domainFromAcct else { return } + Task { + let account = try await self.context.apiService.fetchUser( + username: account.username, + domain: domain, + authenticationBox: self.authContext.mastodonAuthenticationBox + ) + + guard let account else { return } + + let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: self.authContext.mastodonAuthenticationBox).value.first + + guard let relationship else { return } + + // they don't run as there's not a change, probably? + self.viewModel.relationship = relationship + self.viewModel.account = account + } + }) + .store(in: &disposeBag) } private func bindMoreBarButtonItem() { @@ -667,78 +678,82 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton ) { + // let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none #warning("TODO: Implement") // handle edit logic for editable profile // handle relationship logic for non-editable profile -// if relationshipActionSet.contains(.edit) { + if let me = viewModel.me, me == viewModel.account { // // do nothing when updating -// guard !viewModel.isUpdating else { return } -// -// guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } -// guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } -// -// let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited -// -// if isEdited { -// // update profile when edited -// viewModel.isUpdating = true -// Task { @MainActor in -// do { -// // TODO: handle error -// _ = try await viewModel.updateProfileInfo( -// headerProfileInfo: profileHeaderViewModel.profileInfoEditing, -// aboutProfileInfo: profileAboutViewModel.profileInfoEditing -// ) -// self.viewModel.isEditing = false -// -// } catch { -// let alertController = UIAlertController( -// for: error, -// title: L10n.Common.Alerts.EditProfileFailure.title, -// preferredStyle: .alert -// ) -// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) -// alertController.addAction(okAction) -// self.present(alertController, animated: true) -// } -// -// // finish updating -// self.viewModel.isUpdating = false -// } // end Task -// } else { -// // set `updating` then toggle `edit` state -// viewModel.isUpdating = true -// viewModel.fetchEditProfileInfo() -// .receive(on: DispatchQueue.main) -// .sink { [weak self] completion in -// guard let self = self else { return } -// defer { -// // finish updating -// self.viewModel.isUpdating = false -// } -// switch completion { -// case .failure(let error): -// let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) -// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) -// alertController.addAction(okAction) -// _ = self.coordinator.present( -// scene: .alertController(alertController: alertController), -// from: nil, -// transition: .alertController(animated: true, completion: nil) -// ) -// case .finished: -// // enter editing mode -// self.viewModel.isEditing.toggle() -// } -// } receiveValue: { [weak self] response in -// guard let self = self else { return } -// self.viewModel.accountForEdit = response.value -// } -// .store(in: &disposeBag) -// } -// } else { + guard !viewModel.isUpdating else { return } + + guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } + guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } + + let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited + + if isEdited { + // update profile when edited + viewModel.isUpdating = true + Task { @MainActor in + do { + // TODO: handle error + _ = try await viewModel.updateProfileInfo( + headerProfileInfo: profileHeaderViewModel.profileInfoEditing, + aboutProfileInfo: profileAboutViewModel.profileInfoEditing + ) + self.viewModel.isEditing = false + + } catch { + let alertController = UIAlertController( + for: error, + title: L10n.Common.Alerts.EditProfileFailure.title, + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) + alertController.addAction(okAction) + self.present(alertController, animated: true) + } + + // finish updating + self.viewModel.isUpdating = false + } // end Task + } else { + // set `updating` then toggle `edit` state + viewModel.isUpdating = true + viewModel.fetchEditProfileInfo() + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + defer { + // finish updating + self.viewModel.isUpdating = false + } + switch completion { + case .failure(let error): + let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + _ = self.coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + case .finished: + // enter editing mode + self.viewModel.isEditing.toggle() + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + self.viewModel.accountForEdit = response.value + } + .store(in: &disposeBag) + } + } else { // guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } + guard let relationship = viewModel.relationship else { return } + + print(relationship) // switch relationshipAction { // case .none: // break @@ -800,8 +815,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { // case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: // break // } -// } - + } } func profileHeaderViewController( diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 7c705ecc8..84038435b 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -100,24 +100,31 @@ class ProfileViewModel: NSObject { $userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier) // bind bar button items -#warning("TODO: Implement") -// relationshipViewModel.$optionSet -// .sink { [weak self] optionSet in -// guard let self = self else { return } -// guard let optionSet = optionSet, !optionSet.contains(.none) else { -// self.isReplyBarButtonItemHidden = true -// self.isMoreMenuBarButtonItemHidden = true -// self.isMeBarButtonItemsHidden = true -// return -// } -// -// let isMyself = optionSet.contains(.isMyself) -// self.isReplyBarButtonItemHidden = isMyself -// self.isMoreMenuBarButtonItemHidden = isMyself -// self.isMeBarButtonItemsHidden = !isMyself -// } -// .store(in: &disposeBag) + Publishers.CombineLatest3($account, $me, $relationship) + .sink(receiveValue: { [weak self] account, me, relationship in + guard let self else { + self?.isReplyBarButtonItemHidden = true + self?.isMoreMenuBarButtonItemHidden = true + self?.isMeBarButtonItemsHidden = true + return + } + let isMyself = (account == me) + self.isReplyBarButtonItemHidden = isMyself + self.isMoreMenuBarButtonItemHidden = isMyself + self.isMeBarButtonItemsHidden = (isMyself == false) + }) + .store(in: &disposeBag) + + viewDidAppear + .sink { [weak self] _ in + guard let self else { return } + + self.isReplyBarButtonItemHidden = self.isReplyBarButtonItemHidden + self.isMoreMenuBarButtonItemHidden = self.isMoreMenuBarButtonItemHidden + self.isMeBarButtonItemsHidden = self.isMeBarButtonItemsHidden + } + .store(in: &disposeBag) // query relationship #warning("TODO: Implement") // let pendingRetryPublisher = CurrentValueSubject(1) @@ -151,26 +158,22 @@ class ProfileViewModel: NSObject { // } // .store(in: &disposeBag) -// let isBlockingOrBlocked = Publishers.CombineLatest( -// relationshipViewModel.$isBlocking, -// relationshipViewModel.$isBlockingBy -// ) -// .map { $0 || $1 } -// .share() -// -// Publishers.CombineLatest( -// isBlockingOrBlocked, -// $isEditing -// ) -// .map { !$0 && !$1 } -// .assign(to: &$isPagingEnabled) + let isBlockingOrBlocked = Publishers.CombineLatest3( + (relationship?.blocking ?? false).publisher, + (relationship?.blockedBy ?? false).publisher, + (relationship?.domainBlocking ?? false).publisher + ) + .map { $0 || $1 || $2 } + .share() + + Publishers.CombineLatest( + isBlockingOrBlocked, + $isEditing + ) + .map { !$0 && !$1 } + .assign(to: &$isPagingEnabled) } - - func viewDidLoad() { - - } - // fetch profile info before edit func fetchEditProfileInfo() -> AnyPublisher, Error> { guard let me, let domain = me.domain else { From 0729fa41f2882e788b535f664136e621d92c1fe4 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 24 Dec 2023 15:20:51 +0100 Subject: [PATCH 067/159] Re-enable "Follows you"-indicator (IOS-192) --- .../View/ProfileHeaderView+ViewModel.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index c0cb383f7..b14e373d5 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -98,14 +98,15 @@ extension ProfileHeaderView.ViewModel { } .store(in: &disposeBag) // follows you -#warning("TODO: Implement") -// $relationshipActionOptionSet -// .map { $0.contains(.followingBy) && !$0.contains(.isMyself) } -// .receive(on: DispatchQueue.main) -// .sink { isFollowingBy in -// view.followsYouBlurEffectView.isHidden = !isFollowingBy -// } -// .store(in: &disposeBag) + Publishers.CombineLatest($relationship, $isMyself) + .map { relationship, isMyself in + (relationship?.following ?? false) && (isMyself == false) } + .receive(on: DispatchQueue.main) + .sink { isFollowing in + view.followsYouBlurEffectView.isHidden = (isFollowing == false) + } + .store(in: &disposeBag) + // avatar Publishers.CombineLatest4( $avatarImageURL, From b7bb271b40cd8cbae63f3bf3ebf89ff3ced94a26 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sun, 24 Dec 2023 15:54:16 +0100 Subject: [PATCH 068/159] Re-enable blur-effect for pictures (IOS-192) --- .../View/ProfileHeaderView+ViewModel.swift | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index b14e373d5..7997b7791 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -121,19 +121,22 @@ extension ProfileHeaderView.ViewModel { )) } .store(in: &disposeBag) -#warning("TODO: Implement") -// // blur for blocking & blockingBy -// $relationshipActionOptionSet -// .map { $0.contains(.blocking) || $0.contains(.blockingBy) } -// .sink { needsImageOverlayBlurred in -// UIView.animate(withDuration: 0.33) { -// let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil -// view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect -// let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil -// view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect -// } -// } -// .store(in: &disposeBag) + // blur for blocking & blockingBy + $relationship + .compactMap { relationship in + guard let relationship else { return false } + + return relationship.blocking || (relationship.blockedBy ?? false) + } + .sink { needsImageOverlayBlurred in + UIView.animate(withDuration: 0.33) { + let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil + view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect + let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil + view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect + } + } + .store(in: &disposeBag) // name Publishers.CombineLatest4( $isEditing.removeDuplicates(), From 2f0614d9c9a8d8f0ca3704eb814c186b2051caed Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 25 Dec 2023 14:38:11 +0100 Subject: [PATCH 069/159] Refactor Mute/Block/HideReblogs to work with relationships (IOS-192) Also: Connect Blocking/BlockedBy --- .../Provider/DataSourceFacade+Block.swift | 7 +- .../Provider/DataSourceFacade+Mute.swift | 8 +- .../Provider/DataSourceFacade+Status.swift | 19 +++-- ...er+NotificationTableViewCellDelegate.swift | 4 +- .../Scene/Profile/ProfileViewController.swift | 84 ++++++++++++++----- 5 files changed, 89 insertions(+), 33 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index c87217595..ff4a615a4 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -35,14 +35,14 @@ extension DataSourceFacade { static func responseToUserBlockAction( dependency: NeedsDependency & AuthContextProvider, user: Mastodon.Entity.Account - ) async throws { + ) async throws -> Mastodon.Entity.Relationship { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() let apiService = dependency.context.apiService let authBox = dependency.authContext.mastodonAuthenticationBox - _ = try await apiService.toggleBlock( + let response = try await apiService.toggleBlock( user: user, authenticationBox: authBox ) @@ -50,6 +50,9 @@ extension DataSourceFacade { try await dependency.context.apiService.getBlocked( authenticationBox: authBox ) + dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + + return response.value } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 4e37bc7a5..68462e462 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -13,13 +13,15 @@ extension DataSourceFacade { static func responseToUserMuteAction( dependency: NeedsDependency & AuthContextProvider, account: Mastodon.Entity.Account - ) async throws { + ) async throws -> Mastodon.Entity.Relationship { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - _ = try await dependency.context.apiService.toggleMute( + let response = try await dependency.context.apiService.toggleMute( authenticationBox: dependency.authContext.mastodonAuthenticationBox, account: account ) - } // end func + + return response.value + } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 9147569a7..f52968e27 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -151,10 +151,11 @@ extension DataSourceFacade { } @MainActor - static func responseToMenuAction( + static func responseToMenuAction( dependency: UIViewController & NeedsDependency & AuthContextProvider & DataSourceProvider, action: MastodonMenu.Action, - menuContext: MenuContext + menuContext: MenuContext, + completion: ((T) -> Void)? = { (param: Void) in } ) async throws { switch action { case .hideReblogs(let actionContext): @@ -200,11 +201,15 @@ extension DataSourceFacade { ) { [weak dependency] _ in guard let dependency else { return } Task { - try await DataSourceFacade.responseToUserMuteAction( + let newRelationship = try await DataSourceFacade.responseToUserMuteAction( dependency: dependency, account: menuContext.author ) - } // end Task + + if let completion { + completion(newRelationship as! T) + } + } } alertController.addAction(confirmAction) let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) @@ -222,10 +227,14 @@ extension DataSourceFacade { ) { [weak dependency] _ in guard let dependency else { return } Task { - try await DataSourceFacade.responseToUserBlockAction( + let newRelationship = try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, user: menuContext.author ) + + if let completion { + completion(newRelationship as! T) + } } } alertController.addAction(confirmAction) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index e1d01a357..e9572ce3c 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -30,8 +30,8 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut assertionFailure("only works for status data provider") return } - - try await DataSourceFacade.responseToMenuAction( + + _ = try await DataSourceFacade.responseToMenuAction( dependency: self, action: action, menuContext: .init( diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 6b6488f32..814208385 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -316,17 +316,28 @@ extension ProfileViewController { viewModel.$accountForEdit .assign(to: \.accountForEdit, on: headerViewModel) .store(in: &disposeBag) -#warning("TODO: Implement") - // timeline -// [ -// viewModel.postsUserTimelineViewModel, -// viewModel.repliesUserTimelineViewModel, -// viewModel.mediaUserTimelineViewModel, -// ].forEach { userTimelineViewModel in -// viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag) -// viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag) -// viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag) -// } + + [ + viewModel.postsUserTimelineViewModel, + viewModel.repliesUserTimelineViewModel, + viewModel.mediaUserTimelineViewModel, + ].forEach { userTimelineViewModel in + + viewModel.relationship.publisher + .map { $0.blocking } + .assign(to: \UserTimelineViewModel.isBlocking, on: userTimelineViewModel) + .store(in: &disposeBag) + + viewModel.relationship.publisher + .compactMap { $0.blockedBy } + .assign(to: \UserTimelineViewModel.isBlockedBy, on: userTimelineViewModel) + .store(in: &disposeBag) + + viewModel.$account + .compactMap { $0.suspended } + .assign(to: \UserTimelineViewModel.isSuspended, on: userTimelineViewModel) + .store(in: &disposeBag) + } // about let aboutViewModel = viewModel.profileAboutViewModel @@ -843,18 +854,49 @@ extension ProfileViewController: ProfileAboutViewControllerDelegate { // MARK: - MastodonMenuDelegate extension ProfileViewController: MastodonMenuDelegate { func menuAction(_ action: MastodonMenu.Action) { + switch action { + case .muteUser(_), + .blockUser(_), + .hideReblogs(_): + Task { + try await DataSourceFacade.responseToMenuAction( + dependency: self, + action: action, + menuContext: DataSourceFacade.MenuContext( + author: viewModel.account, + statusViewModel: nil, + button: nil, + barButtonItem: self.moreMenuBarButtonItem + )) { [weak self] newRelationship in + guard let self else { return } + + self.viewModel.relationship = newRelationship + } + } + case .reportUser(_), .shareUser(_): + Task { + try await DataSourceFacade.responseToMenuAction( + dependency: self, + action: action, + menuContext: DataSourceFacade.MenuContext( + author: viewModel.account, + statusViewModel: nil, + button: nil, + barButtonItem: self.moreMenuBarButtonItem + )) + } + + case .translateStatus(_), + .showOriginal, + .bookmarkStatus(_), + .shareStatus, + .deleteStatus, + .editStatus, + .followUser(_): + break + } Task { - try await DataSourceFacade.responseToMenuAction( - dependency: self, - action: action, - menuContext: DataSourceFacade.MenuContext( - author: viewModel.account, - statusViewModel: nil, - button: nil, - barButtonItem: self.moreMenuBarButtonItem - ) - ) } } } From f9492e07a3e8085f2f245cb780ec845295a451d0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 25 Dec 2023 23:42:30 +0100 Subject: [PATCH 070/159] Kill some forced unwrapping and hide bio when blocked/blocking (IOS-192) First rule of optionals: Don't use forced unwrapping! --- .../Header/ProfileHeaderViewController.swift | 24 +++++++++---- .../View/ProfileHeaderView+ViewModel.swift | 36 +++++++++++-------- .../Header/View/ProfileHeaderView.swift | 31 +++++++--------- .../Scene/Profile/ProfileViewController.swift | 11 +++--- 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 8b5a0c8b8..3112dd41e 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -18,6 +18,7 @@ import MastodonCore import MastodonUI import MastodonLocalization import TabBarPager +import MastodonSDK protocol ProfileHeaderViewControllerDelegate: AnyObject { func profileHeaderViewController(_ profileHeaderViewController: ProfileHeaderViewController, profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) @@ -29,12 +30,12 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi static let segmentedControlHeight: CGFloat = 50 static let headerMinHeight: CGFloat = segmentedControlHeight - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var context: AppContext! weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - var viewModel: ProfileHeaderViewModel! - + let viewModel: ProfileHeaderViewModel + weak var delegate: ProfileHeaderViewControllerDelegate? weak var headerDelegate: TabBarPagerHeaderDelegate? @@ -51,7 +52,7 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi return titleView }() - let profileHeaderView = ProfileHeaderView() + let profileHeaderView: ProfileHeaderView // private var isBannerPinned = false @@ -81,11 +82,17 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi return documentPickerController }() - -} + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, coordinator: SceneCoordinator) { + self.context = context + self.coordinator = coordinator + self.viewModel = ProfileHeaderViewModel(context: context, authContext: authContext, account: account) + self.profileHeaderView = ProfileHeaderView(account: account) -extension ProfileHeaderViewController { + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func viewDidLoad() { super.viewDidLoad() @@ -135,6 +142,9 @@ extension ProfileHeaderViewController { viewModel.$relationship .assign(to: \.relationship, on: profileHeaderView.viewModel) .store(in: &disposeBag) + viewModel.$account + .assign(to: \.account, on: profileHeaderView.viewModel) + .store(in: &disposeBag) viewModel.$isMyself .assign(to: \.isMyself, on: profileHeaderView.viewModel) .store(in: &disposeBag) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 7997b7791..aa1ff777d 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -46,12 +46,15 @@ extension ProfileHeaderView { @Published var fields: [MastodonField] = [] + @Published var account: Mastodon.Entity.Account @Published var relationship: Mastodon.Entity.Relationship? @Published var isRelationshipActionButtonHidden = false @Published var isMyself = false - init() { -#warning("TODO: Implement") + init(account: Mastodon.Entity.Account) { + self.account = account + + #warning("TODO: Implement") // $relationshipActionOptionSet // .compactMap { $0.highPriorityAction(except: []) } // .map { $0 == .none } @@ -190,18 +193,23 @@ extension ProfileHeaderView.ViewModel { view.bioMetaText.configure(content: metaContent) } .store(in: &disposeBag) -#warning("TODO: Implement") - // $relationshipActionOptionSet -// .receive(on: DispatchQueue.main) -// .sink { optionSet in -// let isBlocking = optionSet.contains(.blocking) -// let isBlockedBy = optionSet.contains(.blockingBy) -// let isSuspended = optionSet.contains(.suspended) -// let isNeedsHidden = isBlocking || isBlockedBy || isSuspended -// -// view.bioMetaText.textView.isHidden = isNeedsHidden -// } -// .store(in: &disposeBag) + + Publishers.CombineLatest($relationship, $account) + .compactMap { relationship, account in + guard let relationship else { return nil } + let isBlocking = relationship.blocking + let isBlockedBy = relationship.blockedBy ?? false + let isSuspended = account.suspended ?? false + let isNeedsHidden = isBlocking || isBlockedBy || isSuspended + + return isNeedsHidden + } + .receive(on: DispatchQueue.main) + .sink { isNeedsHidden in + view.bioMetaText.textView.isHidden = isNeedsHidden + } + .store(in: &disposeBag) + // dashboard $isMyself .receive(on: DispatchQueue.main) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index da9dd82e6..fe7dcaae0 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -13,6 +13,7 @@ import MastodonAsset import MastodonCore import MastodonLocalization import MastodonUI +import MastodonSDK protocol ProfileHeaderViewDelegate: AnyObject { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) @@ -42,12 +43,8 @@ final class ProfileHeaderView: UIView { disposeBag.removeAll() } - private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(view: self) - return viewModel - }() - + private(set) var viewModel: ViewModel + let bannerImageViewSingleTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer let bannerContainerView = UIView() let bannerImageView: UIImageView = { @@ -238,20 +235,14 @@ final class ProfileHeaderView: UIView { return metaText }() - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} + init(account: Mastodon.Entity.Account) { + + viewModel = ViewModel(account: account) + + super.init(frame: .zero) + + viewModel.bind(view: self) -extension ProfileHeaderView { - private func _init() { setColors() // banner @@ -460,6 +451,8 @@ extension ProfileHeaderView { updateLayoutMargins() } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setColors() { backgroundColor = .systemBackground diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 814208385..d06e3d0a2 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -119,10 +119,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi private(set) lazy var tabBarPagerController = TabBarPagerController() private(set) lazy var profileHeaderViewController: ProfileHeaderViewController = { - let viewController = ProfileHeaderViewController() - viewController.context = context - viewController.coordinator = coordinator - viewController.viewModel = ProfileHeaderViewModel(context: context, authContext: viewModel.authContext, account: viewModel.account) + let viewController = ProfileHeaderViewController(context: context, authContext: authContext, account: viewModel.account, coordinator: coordinator) return viewController }() @@ -300,7 +297,7 @@ extension ProfileViewController { private func bindViewModel() { // header - let headerViewModel = profileHeaderViewController.viewModel! + let headerViewModel = profileHeaderViewController.viewModel viewModel.$account .assign(to: \.account, on: headerViewModel) .store(in: &disposeBag) @@ -698,7 +695,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { // // do nothing when updating guard !viewModel.isUpdating else { return } - guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } + let profileHeaderViewModel = profileHeaderViewController.viewModel guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited @@ -885,7 +882,7 @@ extension ProfileViewController: MastodonMenuDelegate { barButtonItem: self.moreMenuBarButtonItem )) } - + case .translateStatus(_), .showOriginal, .bookmarkStatus(_), From 393722a31da64134b6cda97d774e26d482057fc6 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 25 Dec 2023 23:42:49 +0100 Subject: [PATCH 071/159] Reenable followers/followees (IOS-192) --- .../Header/ProfileHeaderViewController.swift | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 3112dd41e..0458ed9ae 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -336,41 +336,37 @@ extension ProfileHeaderViewController: ProfileHeaderViewDelegate { switch meter { case .post: // do nothing - break - case .follower: -#warning("TODO: Implement") -// guard let domain = viewModel.account.domain, -// let userID = viewModel.account.id -// else { return } -// let followerListViewModel = FollowerListViewModel( -// context: context, -// authContext: viewModel.authContext, -// domain: domain, -// userID: userID -// ) -// _ = coordinator.present( -// scene: .follower(viewModel: followerListViewModel), -// from: self, -// transition: .show -// ) - break - case .following: -#warning("TODO: Implement") -// guard let domain = viewModel.account.domain, -// let userID = viewModel.account.id -// else { return } -// let followingListViewModel = FollowingListViewModel( -// context: context, -// authContext: viewModel.authContext, -// domain: domain, -// userID: userID -// ) -// _ = coordinator.present( -// scene: .following(viewModel: followingListViewModel), -// from: self, -// transition: .show -// ) break + case .follower: + guard let domain = viewModel.account.domain else { return } + let userID = viewModel.account.id + let followerListViewModel = FollowerListViewModel( + context: context, + authContext: viewModel.authContext, + domain: domain, + userID: userID + ) + _ = coordinator.present( + scene: .follower(viewModel: followerListViewModel), + from: self, + transition: .show + ) + + case .following: + guard let domain = viewModel.account.domain else { return } + + let userID = viewModel.account.id + let followingListViewModel = FollowingListViewModel( + context: context, + authContext: viewModel.authContext, + domain: domain, + userID: userID + ) + _ = coordinator.present( + scene: .following(viewModel: followingListViewModel), + from: self, + transition: .show + ) } } From 76304e59e518868a4b2bae189bd3988267ad1302 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 26 Dec 2023 16:05:48 +0100 Subject: [PATCH 072/159] Configure Profile-button based on relationship and accounts (IOS-192) Also `me` is not optional anymore as we need it --- Mastodon/Coordinator/SceneCoordinator.swift | 4 ++- .../Provider/DataSourceFacade+Profile.swift | 6 ++-- .../Header/ProfileHeaderViewController.swift | 6 ++-- .../Header/ProfileHeaderViewModel.swift | 5 ++- .../View/ProfileHeaderView+ViewModel.swift | 32 ++++++++++++------- .../Header/View/ProfileHeaderView.swift | 4 +-- .../Scene/Profile/ProfileViewController.swift | 11 ++++--- Mastodon/Scene/Profile/ProfileViewModel.swift | 10 +++--- .../Root/MainTab/MainTabBarController.swift | 2 +- Mastodon/Supporting Files/SceneDelegate.swift | 10 ++++-- .../ProfileRelationshipActionButton.swift | 27 ++++++++++++++++ 11 files changed, 83 insertions(+), 34 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 1b9e99521..8f1c9cc3d 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -98,6 +98,7 @@ final public class SceneCoordinator { // show notification related content guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } guard let authContext = self.authContext else { return } + guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return } let notificationID = String(pushNotification.notificationID) switch type { @@ -114,7 +115,8 @@ final public class SceneCoordinator { context: appContext, authContext: authContext, account: account, - relationship: relationship + relationship: relationship, + me: me ) _ = self.present( scene: .profile(viewModel: profileViewModel), diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 24e059f1c..49aa61316 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -104,13 +104,15 @@ extension DataSourceFacade { Task { @MainActor in - guard let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else { return } + guard let me = provider.authContext.mastodonAuthenticationBox.authentication.account(), + let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else { return } let profileViewModel = ProfileViewModel( context: provider.context, authContext: provider.authContext, account: account, - relationship: relationship + relationship: relationship, + me: me ) _ = provider.coordinator.present( diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 0458ed9ae..a4f8af02d 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -82,11 +82,11 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi return documentPickerController }() - init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, coordinator: SceneCoordinator) { + init(context: AppContext, authContext: AuthContext, coordinator: SceneCoordinator, profileViewModel: ProfileViewModel) { self.context = context self.coordinator = coordinator - self.viewModel = ProfileHeaderViewModel(context: context, authContext: authContext, account: account) - self.profileHeaderView = ProfileHeaderView(account: account) + self.viewModel = ProfileHeaderViewModel(context: context, authContext: authContext, account: profileViewModel.account, me: profileViewModel.me, relationship: profileViewModel.relationship) + self.profileHeaderView = ProfileHeaderView(account: profileViewModel.account, me: profileViewModel.me, relationship: profileViewModel.relationship) super.init(nibName: nil, bundle: nil) } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index f8db96e06..11a499109 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -26,6 +26,7 @@ final class ProfileHeaderViewModel { let context: AppContext let authContext: AuthContext + @Published var me: Mastodon.Entity.Account @Published var account: Mastodon.Entity.Account @Published var relationship: Mastodon.Entity.Relationship? @@ -44,10 +45,12 @@ final class ProfileHeaderViewModel { @Published var isTitleViewDisplaying = false @Published var isTitleViewContentOffsetSet = false - init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account) { + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, me: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) { self.context = context self.authContext = authContext self.account = account + self.me = me + self.relationship = relationship $accountForEdit .receive(on: DispatchQueue.main) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index aa1ff777d..aabb4f9d5 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -46,13 +46,16 @@ extension ProfileHeaderView { @Published var fields: [MastodonField] = [] + @Published var me: Mastodon.Entity.Account @Published var account: Mastodon.Entity.Account @Published var relationship: Mastodon.Entity.Relationship? @Published var isRelationshipActionButtonHidden = false @Published var isMyself = false - init(account: Mastodon.Entity.Account) { + init(account: Mastodon.Entity.Account, me: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) { self.account = account + self.me = me + self.relationship = relationship #warning("TODO: Implement") // $relationshipActionOptionSet @@ -103,7 +106,8 @@ extension ProfileHeaderView.ViewModel { // follows you Publishers.CombineLatest($relationship, $isMyself) .map { relationship, isMyself in - (relationship?.following ?? false) && (isMyself == false) } + return (relationship?.following ?? false) && (isMyself == false) + } .receive(on: DispatchQueue.main) .sink { isFollowing in view.followsYouBlurEffectView.isHidden = (isFollowing == false) @@ -196,7 +200,9 @@ extension ProfileHeaderView.ViewModel { Publishers.CombineLatest($relationship, $account) .compactMap { relationship, account in + guard let relationship else { return nil } + let isBlocking = relationship.blocking let isBlockedBy = relationship.blockedBy ?? false let isSuspended = account.suspended ?? false @@ -263,13 +269,17 @@ extension ProfileHeaderView.ViewModel { .assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer) .store(in: &disposeBag) #warning("TODO: Implement") -// Publishers.CombineLatest2( -// $relationshipActionOptionSet, -// $isEditing, -// $isUpdating -// ) -// .receive(on: DispatchQueue.main) -// .sink { relationshipActionOptionSet, isEditing, isUpdating in + Publishers.CombineLatest3( + Publishers.CombineLatest3($me, $account, $relationship).eraseToAnyPublisher(), + $isEditing, + $isUpdating + ) + .receive(on: DispatchQueue.main) + .sink { tuple, isEditing, isUpdating in + let (me, account, relationship) = tuple + guard let relationship else { return } + + view.relationshipActionButton.configure(relationship: relationship, between: account, and: me, isEditing: isEditing, isUpdating: isUpdating) // if relationshipActionOptionSet.contains(.edit) { // // check .edit state and set .editing when isEditing // view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit)) @@ -277,8 +287,8 @@ extension ProfileHeaderView.ViewModel { // } else { // view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet) // } -// } -// .store(in: &disposeBag) + } + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index fe7dcaae0..7b0190b10 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -235,9 +235,9 @@ final class ProfileHeaderView: UIView { return metaText }() - init(account: Mastodon.Entity.Account) { + init(account: Mastodon.Entity.Account, me: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) { - viewModel = ViewModel(account: account) + viewModel = ViewModel(account: account, me: me, relationship: relationship) super.init(frame: .zero) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index d06e3d0a2..62dfb018d 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -119,7 +119,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi private(set) lazy var tabBarPagerController = TabBarPagerController() private(set) lazy var profileHeaderViewController: ProfileHeaderViewController = { - let viewController = ProfileHeaderViewController(context: context, authContext: authContext, account: viewModel.account, coordinator: coordinator) + let viewController = ProfileHeaderViewController(context: context, authContext: authContext, coordinator: coordinator, profileViewModel: viewModel) return viewController }() @@ -691,7 +691,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { #warning("TODO: Implement") // handle edit logic for editable profile // handle relationship logic for non-editable profile - if let me = viewModel.me, me == viewModel.account { + if viewModel.me == viewModel.account { // // do nothing when updating guard !viewModel.isUpdating else { return } @@ -706,12 +706,13 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { Task { @MainActor in do { // TODO: handle error - _ = try await viewModel.updateProfileInfo( + let updatedAccount = try await viewModel.updateProfileInfo( headerProfileInfo: profileHeaderViewModel.profileInfoEditing, aboutProfileInfo: profileAboutViewModel.profileInfoEditing - ) + ).value self.viewModel.isEditing = false - + self.viewModel.account = updatedAccount + } catch { let alertController = UIAlertController( for: error, diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 84038435b..68e13bde0 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -34,7 +34,7 @@ class ProfileViewModel: NSObject { let context: AppContext let authContext: AuthContext - @Published var me: Mastodon.Entity.Account? + @Published var me: Mastodon.Entity.Account @Published var account: Mastodon.Entity.Account @Published var relationship: Mastodon.Entity.Relationship? @@ -56,11 +56,12 @@ class ProfileViewModel: NSObject { // let needsPagePinToTop = CurrentValueSubject(false) @MainActor - init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) { + init(context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, me: Mastodon.Entity.Account) { self.context = context self.authContext = authContext self.account = account self.relationship = relationship + self.me = me self.postsUserTimelineViewModel = UserTimelineViewModel( context: context, @@ -82,9 +83,6 @@ class ProfileViewModel: NSObject { ) self.profileAboutViewModel = ProfileAboutViewModel(context: context, account: account) super.init() - - // bind me - self.me = authContext.mastodonAuthenticationBox.authentication.account() // bind user $account @@ -176,7 +174,7 @@ class ProfileViewModel: NSObject { // fetch profile info before edit func fetchEditProfileInfo() -> AnyPublisher, Error> { - guard let me, let domain = me.domain else { + guard let domain = me.domain else { return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index f247b83cc..d4e587758 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -119,7 +119,7 @@ class MainTabBarController: UITabBarController { let _viewController = ProfileViewController() _viewController.context = context _viewController.coordinator = coordinator - _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil) + _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me) viewController = _viewController } viewController.title = self.title diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 3bff14995..6cfb3b482 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -141,6 +141,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let domain = authContext.mastodonAuthenticationBox.domain let authenticationBox = authContext.mastodonAuthenticationBox + guard let me = authenticationBox.authentication.account() else { return } + guard let account = try await AppContext.shared.apiService.search( query: .init(q: incomingURL.absoluteString, type: .accounts, resolve: true), authenticationBox: authenticationBox @@ -155,7 +157,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { context: AppContext.shared, authContext: authContext, account: account, - relationship: relationship + relationship: relationship, + me: me ) _ = self.coordinator?.present( scene: .profile(viewModel: profileViewModel), @@ -285,6 +288,8 @@ extension SceneDelegate { let domain = authContext.mastodonAuthenticationBox.domain let authenticationBox = authContext.mastodonAuthenticationBox + guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return } + guard let account = try await AppContext.shared.apiService.search( query: .init(q: components[1], type: .accounts, resolve: true), authenticationBox: authenticationBox @@ -299,7 +304,8 @@ extension SceneDelegate { context: AppContext.shared, authContext: authContext, account: account, - relationship: relationship + relationship: relationship, + me: me ) self.coordinator?.present( diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift index 46ec8b5dd..56ef81b01 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift @@ -7,6 +7,7 @@ import UIKit import MastodonAsset +import MastodonSDK import MastodonLocalization public final class ProfileRelationshipActionButton: RoundedEdgesButton { @@ -55,6 +56,32 @@ extension ProfileRelationshipActionButton { } extension ProfileRelationshipActionButton { + + public func configure(relationship: Mastodon.Entity.Relationship, between user: Mastodon.Entity.Account, and me: Mastodon.Entity.Account, isEditing: Bool = false, isUpdating: Bool = false) { + + let isMyself = (user == me) + let title: String + + if isMyself { + if isEditing { + title = "SAVE" + } else { + title = "EDIT" + } + } else if relationship.following { + title = L10n.Common.Controls.Friendship.follow + } else { + title = "TITLE" + } + setTitle(title, for: .normal) + + if relationship.blocking || user.suspended ?? false { + isEnabled = false + } else { + isEnabled = true + } + } + public func configure(actionOptionSet: RelationshipActionOptionSet) { setTitle(actionOptionSet.title, for: .normal) From 1b1274c2f7152c03007f0d0e865566af9a410e9c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 26 Dec 2023 16:27:04 +0100 Subject: [PATCH 073/159] Show correct account for "quoted" posts in notifications (IOS-192) --- ...DataSourceProvider+NotificationTableViewCellDelegate.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index e9572ce3c..029aec5d4 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -305,9 +305,11 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } + guard let account = notification.status?.entity.account else { return } + await DataSourceFacade.coordinateToProfileScene( provider: self, - account: notification.entity.account + account: account ) } // end Task } From dda0ae2d7cfe162570b167abce7a38b293928626 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 27 Dec 2023 15:45:06 +0100 Subject: [PATCH 074/159] Show text on relationship-button based on relationship (IOS-192) --- .../Persistence+MastodonUser.swift | 8 ++--- .../Service/API/APIService+Follow.swift | 2 +- .../Service/API/APIService+Mute.swift | 2 +- .../Entity/Mastodon+Entity+Relationship.swift | 33 ++++++++++++------- .../Content/StatusView+Configuration.swift | 2 +- .../MastodonUI/View/Content/UserView.swift | 2 +- .../ProfileRelationshipActionButton.swift | 26 +++++++++++---- 7 files changed, 49 insertions(+), 26 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift index b8b5f3089..a36863458 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift @@ -150,10 +150,10 @@ extension Persistence.MastodonUser { relationship.requested.flatMap { user.update(isFollowRequested: $0, by: me) } // relationship.endorsed.flatMap { user.update(isEndorsed: $0, by: me) } me.update(isFollowing: relationship.followedBy, by: user) - relationship.muting.flatMap { user.update(isMuting: $0, by: me) } + user.update(isMuting: relationship.muting, by: me) user.update(isBlocking: relationship.blocking, by: me) - relationship.domainBlocking.flatMap { user.update(isDomainBlocking: $0, by: me) } - relationship.blockedBy.flatMap { me.update(isBlocking: $0, by: user) } - relationship.showingReblogs.flatMap { me.update(isShowingReblogs: $0, by: user) } + user.update(isDomainBlocking: relationship.domainBlocking, by: me) + me.update(isBlocking: relationship.blockedBy, by: user) + me.update(isShowingReblogs: relationship.showingReblogs, by: user) } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index c27e94bc4..f6a5a572f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -72,7 +72,7 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput().value.first - let oldShowReblogs = relationship?.showingReblogs == true + let oldShowReblogs = relationship?.showingReblogs ?? true let newShowReblogs = (oldShowReblogs == false) let response = try await Mastodon.API.Account.follow( diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index b97e405b9..5100da887 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -73,7 +73,7 @@ extension APIService { let muteContext = MastodonMuteContext( targetUserID: account.id, targetUsername: account.username, - isMuting: relationship.muting ?? false + isMuting: relationship.muting ) let result: Result, Error> diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift index f5d2200e7..bd02f9070 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift @@ -17,22 +17,33 @@ extension Mastodon.Entity { /// # Reference /// [Document](https://docs.joinmastodon.org/entities/relationship/) public struct Relationship: Codable, Sendable, Equatable, Hashable { - public typealias ID = String - - public let id: ID + /// The account ID + public let id: String + /// Are you following this user? public let following: Bool + /// Do you have a pending follow request for this user? public let requested: Bool? - public let endorsed: Bool? + /// Are you featuring this user on your profile? + public let endorsed: Bool + /// Are you followed by this user? public let followedBy: Bool - public let muting: Bool? - public let mutingNotifications: Bool? - public let showingReblogs: Bool? - public let notifying: Bool? + /// Are you muting this user? + public let muting: Bool + /// Are you muting notifications from this user? + public let mutingNotifications: Bool + /// Are you receiving this user’s boosts in your home timeline? + public let showingReblogs: Bool + /// Have you enabled notifications for this user? + public let notifying: Bool + /// Are you blocking this user? public let blocking: Bool - public let domainBlocking: Bool? - public let blockedBy: Bool? + /// Are you blocking this user’s domain? + public let domainBlocking: Bool + /// Is this user blocking you? + public let blockedBy: Bool + /// This user’s profile bio public let note: String? - + enum CodingKeys: String, CodingKey { case id case following diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 1818becaf..ede5e8a15 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -248,7 +248,7 @@ extension StatusView { ).singleOutput().value { guard let rel = relationship.first else { return } DispatchQueue.main.async { [self] in - viewModel.isMuting = rel.muting ?? false + viewModel.isMuting = rel.muting viewModel.isBlocking = rel.blocking viewModel.isFollowed = rel.followedBy } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index b58819df5..5c8b66cad 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -267,7 +267,7 @@ public extension UserView { buttonState = .none } else if relationship.following { buttonState = .unfollow - } else if relationship.blocking || (relationship.domainBlocking ?? false) { + } else if relationship.blocking || relationship.domainBlocking { buttonState = .blocked } else if relationship.requested ?? false { buttonState = .pending diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift index 56ef81b01..efc0a8f1f 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift @@ -57,25 +57,37 @@ extension ProfileRelationshipActionButton { extension ProfileRelationshipActionButton { - public func configure(relationship: Mastodon.Entity.Relationship, between user: Mastodon.Entity.Account, and me: Mastodon.Entity.Account, isEditing: Bool = false, isUpdating: Bool = false) { + public func configure(relationship: Mastodon.Entity.Relationship, between account: Mastodon.Entity.Account, and me: Mastodon.Entity.Account, isEditing: Bool = false, isUpdating: Bool = false) { - let isMyself = (user == me) + let isMyself = (account == me) let title: String if isMyself { if isEditing { - title = "SAVE" + title = L10n.Common.Controls.Actions.save } else { - title = "EDIT" + title = L10n.Common.Controls.Friendship.editInfo } + } else if relationship.blocking { + title = L10n.Common.Controls.Friendship.blocked + } else if relationship.domainBlocking { + #warning("Wait for #1198 (Domain Block, IOS-5) to be merged") + title = "Unblock domain" + } else if (relationship.requested ?? false) { + title = L10n.Common.Controls.Friendship.pending + } else if relationship.muting { + title = L10n.Common.Controls.Friendship.muted } else if relationship.following { - title = L10n.Common.Controls.Friendship.follow + title = L10n.Common.Controls.Friendship.following + } else if account.locked { + title = L10n.Common.Controls.Friendship.request } else { - title = "TITLE" + title = L10n.Common.Controls.Friendship.follow } + setTitle(title, for: .normal) - if relationship.blocking || user.suspended ?? false { + if relationship.blocking || account.suspended ?? false { isEnabled = false } else { isEnabled = true From 9e49775201ede0a4825efb8404d49b6504428dc3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 27 Dec 2023 19:01:01 +0100 Subject: [PATCH 075/159] Make image clickable again (IOS-192) --- .../Provider/DataSourceFacade+Media.swift | 92 +++++++------------ .../MediaPreview/MediaPreviewViewModel.swift | 4 +- .../Header/ProfileHeaderViewController.swift | 50 +++++----- 3 files changed, 56 insertions(+), 90 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift index 45622dba4..9608d8257 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift @@ -140,87 +140,61 @@ extension DataSourceFacade { case profileBanner(ProfileHeaderView) } - func thumbnail() async -> UIImage? { - return await imageView.image + func thumbnail() -> UIImage? { + return imageView.image } } @MainActor static func coordinateToMediaPreviewScene( dependency: NeedsDependency & MediaPreviewableViewController, - user: ManagedObjectRecord, + account: Mastodon.Entity.Account, previewContext: ImagePreviewContext ) async throws { - let managedObjectContext = dependency.context.managedObjectContext + + let avatarAssetURL = account.avatar + let headerAssetURL = account.header + + let thumbnail = previewContext.thumbnail() - var _avatarAssetURL: String? - var _headerAssetURL: String? - - try await managedObjectContext.perform { - guard let user = user.object(in: managedObjectContext) else { return } - _avatarAssetURL = user.avatar - _headerAssetURL = user.header + let source: MediaPreviewTransitionItem.Source + switch previewContext.containerView { + case .profileAvatar(let view): source = .profileAvatar(view) + case .profileBanner(let view): source = .profileBanner(view) } - - let thumbnail = await previewContext.thumbnail() - - let source: MediaPreviewTransitionItem.Source = { + + let mediaPreviewTransitionItem = MediaPreviewTransitionItem( + source: source, + previewableViewController: dependency + ) + + let imageView = previewContext.imageView + mediaPreviewTransitionItem.initialFrame = imageView.superview?.convert(imageView.frame, to: nil) + mediaPreviewTransitionItem.image = thumbnail + mediaPreviewTransitionItem.aspectRatio = thumbnail?.size ?? CGSize(width: 100, height: 100) + mediaPreviewTransitionItem.sourceImageViewCornerRadius = { switch previewContext.containerView { - case .profileAvatar(let view): return .profileAvatar(view) - case .profileBanner(let view): return .profileBanner(view) - } - }() - - let mediaPreviewTransitionItem: MediaPreviewTransitionItem = { - let item = MediaPreviewTransitionItem( - source: source, - previewableViewController: dependency - ) - - let imageView = previewContext.imageView - item.initialFrame = { - let initialFrame = imageView.superview!.convert(imageView.frame, to: nil) - assert(initialFrame != .zero) - return initialFrame - }() - - item.image = thumbnail - - item.aspectRatio = { - if let thumbnail = thumbnail { - return thumbnail.size - } - return CGSize(width: 100, height: 100) - }() - - item.sourceImageViewCornerRadius = { - switch previewContext.containerView { case .profileAvatar: return ProfileHeaderView.avatarImageViewCornerRadius case .profileBanner: return 0 - } - }() - - return item + } }() - - - let mediaPreviewItem: MediaPreviewViewModel.PreviewItem = { - switch previewContext.containerView { + + let mediaPreviewItem: MediaPreviewViewModel.PreviewItem + switch previewContext.containerView { case .profileAvatar: - return .profileAvatar(.init( - assetURL: _avatarAssetURL, + mediaPreviewItem = .profileAvatar(.init( + assetURL: avatarAssetURL, thumbnail: thumbnail )) case .profileBanner: - return .profileBanner(.init( - assetURL: _headerAssetURL, + mediaPreviewItem = .profileBanner(.init( + assetURL: headerAssetURL, thumbnail: thumbnail )) - } - }() - + } + guard mediaPreviewItem.isAssetURLValid else { return } diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index a6b604d6f..9a3f5ed90 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -151,8 +151,8 @@ extension MediaPreviewViewModel { return true // default valid case .profileBanner(let item): guard let assertURL = item.assetURL else { return false } - guard !assertURL.hasSuffix("missing.png") else { return false } - return true + + return assertURL.hasSuffix("missing.png") == false } } } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index a4f8af02d..6a8fceff9 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -268,43 +268,35 @@ extension ProfileHeaderViewController { profileHeaderView.avatarButton.alpha = alpha profileHeaderView.editAvatarBackgroundView.alpha = alpha } - + } // MARK: - ProfileHeaderViewDelegate extension ProfileHeaderViewController: ProfileHeaderViewDelegate { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) { -#warning("TODO: Implement") -// guard let user = viewModel.user else { return } -// let record: ManagedObjectRecord = .init(objectID: user.objectID) -// -// Task { -// try await DataSourceFacade.coordinateToMediaPreviewScene( -// dependency: self, -// user: record, -// previewContext: DataSourceFacade.ImagePreviewContext( -// imageView: button.avatarImageView, -// containerView: .profileAvatar(profileHeaderView) -// ) -// ) -// } // end Task + Task { + try await DataSourceFacade.coordinateToMediaPreviewScene( + dependency: self, + account: viewModel.account, + previewContext: DataSourceFacade.ImagePreviewContext( + imageView: button.avatarImageView, + containerView: .profileAvatar(profileHeaderView) + ) + ) + } } func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) { -#warning("TODO: Implement") -// guard let account = viewModel.account else { return } -// let record: ManagedObjectRecord = .init(objectID: user.objectID) -// -// Task { -// try await DataSourceFacade.coordinateToMediaPreviewScene( -// dependency: self, -// user: record, -// previewContext: DataSourceFacade.ImagePreviewContext( -// imageView: imageView, -// containerView: .profileBanner(profileHeaderView) -// ) -// ) -// } // end Task + Task { + try await DataSourceFacade.coordinateToMediaPreviewScene( + dependency: self, + account: viewModel.account, + previewContext: DataSourceFacade.ImagePreviewContext( + imageView: imageView, + containerView: .profileBanner(profileHeaderView) + ) + ) + } } func profileHeaderView( From 59fe79fe49937dd2fc5ce9d9fe0510708976fd95 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 27 Dec 2023 23:41:23 +0100 Subject: [PATCH 076/159] Fix bug in follows-you-label (IOS-192) :facepalm: RTFM (and add some docs) --- .../Profile/Header/View/ProfileHeaderView+ViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index aabb4f9d5..b87928600 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -106,11 +106,11 @@ extension ProfileHeaderView.ViewModel { // follows you Publishers.CombineLatest($relationship, $isMyself) .map { relationship, isMyself in - return (relationship?.following ?? false) && (isMyself == false) + return (relationship?.followedBy ?? false) && (isMyself == false) } .receive(on: DispatchQueue.main) - .sink { isFollowing in - view.followsYouBlurEffectView.isHidden = (isFollowing == false) + .sink { followsYou in + view.followsYouBlurEffectView.isHidden = (followsYou == false) } .store(in: &disposeBag) From 6225c500082d3e0c6184b0b3f930be51e1f2906e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 27 Dec 2023 23:48:16 +0100 Subject: [PATCH 077/159] Add some actions to Relationship-action-button (IOS-192) Well, it's basically just unblock, unmute or follow/unfollow --- .../Provider/DataSourceFacade+Block.swift | 23 +- .../Provider/DataSourceFacade+Status.swift | 2 +- .../Provider/DataSourceFacade+UserView.swift | 8 +- .../Scene/Profile/ProfileViewController.swift | 268 ++++++++++-------- .../ReportResultViewController.swift | 6 +- .../Persistence+MastodonUser.swift | 2 +- .../Service/API/APIService+Block.swift | 50 +--- .../Entity/Mastodon+Entity+Relationship.swift | 2 +- .../ProfileRelationshipActionButton.swift | 2 +- 9 files changed, 160 insertions(+), 203 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index ff4a615a4..33dda8971 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -14,27 +14,6 @@ extension DataSourceFacade { static func responseToUserBlockAction( dependency: NeedsDependency & AuthContextProvider, account: Mastodon.Entity.Account - ) async throws { - let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() - await selectionFeedbackGenerator.selectionChanged() - - let apiService = dependency.context.apiService - let authBox = dependency.authContext.mastodonAuthenticationBox - - _ = try await apiService.toggleBlock( - account: account, - authenticationBox: authBox - ) - - try await dependency.context.apiService.getBlocked( - authenticationBox: authBox - ) - dependency.context.authenticationService.fetchFollowingAndBlockedAsync() - } - - static func responseToUserBlockAction( - dependency: NeedsDependency & AuthContextProvider, - user: Mastodon.Entity.Account ) async throws -> Mastodon.Entity.Relationship { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() @@ -43,7 +22,7 @@ extension DataSourceFacade { let authBox = dependency.authContext.mastodonAuthenticationBox let response = try await apiService.toggleBlock( - user: user, + account: account, authenticationBox: authBox ) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index f52968e27..8ab436994 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -229,7 +229,7 @@ extension DataSourceFacade { Task { let newRelationship = try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, - user: menuContext.author + account: menuContext.author ) if let completion { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift index 8cea808fa..097f430b5 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift @@ -21,21 +21,21 @@ extension DataSourceFacade { dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(account.id) case .request: - _ = try await DataSourceFacade.responseToUserFollowAction( + _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, account: account ) dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(account.id) case .unfollow: - _ = try await DataSourceFacade.responseToUserFollowAction( + _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, account: account ) dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == account.id }) case .blocked: - try await DataSourceFacade.responseToUserBlockAction( + _ = try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, account: account ) @@ -43,7 +43,7 @@ extension DataSourceFacade { dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(account.id) case .pending: - _ = try await DataSourceFacade.responseToUserFollowAction( + _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, account: account ) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 62dfb018d..1e4dc12d0 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -686,62 +686,65 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton ) { - -// let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none -#warning("TODO: Implement") - // handle edit logic for editable profile - // handle relationship logic for non-editable profile if viewModel.me == viewModel.account { -// // do nothing when updating - guard !viewModel.isUpdating else { return } + editProfile() + } else { + editRelationship() + } + } - let profileHeaderViewModel = profileHeaderViewController.viewModel - guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } - - let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited - - if isEdited { - // update profile when edited - viewModel.isUpdating = true - Task { @MainActor in - do { - // TODO: handle error - let updatedAccount = try await viewModel.updateProfileInfo( - headerProfileInfo: profileHeaderViewModel.profileInfoEditing, - aboutProfileInfo: profileAboutViewModel.profileInfoEditing - ).value - self.viewModel.isEditing = false - self.viewModel.account = updatedAccount - } catch { - let alertController = UIAlertController( - for: error, - title: L10n.Common.Alerts.EditProfileFailure.title, - preferredStyle: .alert - ) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) - alertController.addAction(okAction) - self.present(alertController, animated: true) + private func editProfile() { + // do nothing when updating + guard !viewModel.isUpdating else { return } + + let profileHeaderViewModel = profileHeaderViewController.viewModel + guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } + + let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited + + if isEdited { + // update profile when edited + viewModel.isUpdating = true + Task { @MainActor in + do { + // TODO: handle error + let updatedAccount = try await viewModel.updateProfileInfo( + headerProfileInfo: profileHeaderViewModel.profileInfoEditing, + aboutProfileInfo: profileAboutViewModel.profileInfoEditing + ).value + self.viewModel.isEditing = false + self.viewModel.account = updatedAccount + + } catch { + let alertController = UIAlertController( + for: error, + title: L10n.Common.Alerts.EditProfileFailure.title, + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) + alertController.addAction(okAction) + self.present(alertController, animated: true) + } + + // finish updating + self.viewModel.isUpdating = false + } + } else { + // set `updating` then toggle `edit` state + viewModel.isUpdating = true + viewModel.fetchEditProfileInfo() + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + defer { + // finish updating + self.viewModel.isUpdating = false } - - // finish updating - self.viewModel.isUpdating = false - } // end Task - } else { - // set `updating` then toggle `edit` state - viewModel.isUpdating = true - viewModel.fetchEditProfileInfo() - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - defer { - // finish updating - self.viewModel.isUpdating = false - } - switch completion { + switch completion { case .failure(let error): let alertController = UIAlertController(for: error, title: L10n.Common.Alerts.EditProfileFailure.title, preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) alertController.addAction(okAction) _ = self.coordinator.present( scene: .alertController(alertController: alertController), @@ -751,82 +754,101 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { case .finished: // enter editing mode self.viewModel.isEditing.toggle() - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - self.viewModel.accountForEdit = response.value } - .store(in: &disposeBag) - } - } else { -// guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } - guard let relationship = viewModel.relationship else { return } - - print(relationship) -// switch relationshipAction { -// case .none: -// break -// case .follow, .request, .pending, .following: -// guard let user = viewModel.user else { return } -// let record = ManagedObjectRecord(objectID: user.objectID) -// Task { -// try await DataSourceFacade.responseToUserFollowAction( -// dependency: self, -// user: record -// ) -// } -// case .muting: -// guard let user = viewModel.user else { return } -// let name = user.displayNameWithFallback -// -// let alertController = UIAlertController( -// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, -// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), -// preferredStyle: .alert -// ) -// let record = ManagedObjectRecord(objectID: user.objectID) -// let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in -// guard let self = self else { return } -// Task { -// try await DataSourceFacade.responseToUserMuteAction( -// dependency: self, -// user: record -// ) -// } -// } -// alertController.addAction(unmuteAction) -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) -// alertController.addAction(cancelAction) -// present(alertController, animated: true, completion: nil) -// case .blocking: -// guard let user = viewModel.user else { return } -// let name = user.displayNameWithFallback -// -// let alertController = UIAlertController( -// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, -// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), -// preferredStyle: .alert -// ) -// let record = ManagedObjectRecord(objectID: user.objectID) -// let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in -// guard let self = self else { return } -// Task { -// try await DataSourceFacade.responseToUserBlockAction( -// dependency: self, -// user: record -// ) -// } -// } -// alertController.addAction(unblockAction) -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) -// alertController.addAction(cancelAction) -// present(alertController, animated: true, completion: nil) -// case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: -// break -// } + } receiveValue: { [weak self] response in + guard let self = self else { return } + self.viewModel.accountForEdit = response.value + } + .store(in: &disposeBag) } } - + + private func editRelationship() { + guard let relationship = viewModel.relationship else { return } + + let account = viewModel.account + + if relationship.blocking { + let name = account.displayNameWithFallback + + let alertController = UIAlertController( + title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, + message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), + preferredStyle: .alert + ) + let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in + guard let self else { return } + Task { + let newRelationship = try await DataSourceFacade.responseToUserBlockAction( + dependency: self, + account: account + ) + + self.viewModel.relationship = newRelationship + } + } + alertController.addAction(unblockAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) + alertController.addAction(cancelAction) + coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true)) + } else if relationship.muting { + let name = account.displayNameWithFallback + + let alertController = UIAlertController( + title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, + message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), + preferredStyle: .alert + ) + + let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in + guard let self else { return } + Task { + let newRelationship = try await DataSourceFacade.responseToUserMuteAction( + dependency: self, + account: account + ) + + self.viewModel.relationship = newRelationship + } + } + alertController.addAction(unmuteAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) + alertController.addAction(cancelAction) + coordinator.present(scene: .alertController(alertController: alertController), transition: .alertController(animated: true)) + } else { + Task { [weak self] in + guard let self else { return } + + let newRelationship = try await DataSourceFacade.responseToUserFollowAction( + dependency: self, + account: viewModel.account + ) + + self.viewModel.relationship = newRelationship + // update account? + // update me? + } + + } + + // switch relationshipAction { + // case .none: + // break + // case .follow, .request, .pending, .following: + // guard let user = viewModel.user else { return } + // let record = ManagedObjectRecord(objectID: user.objectID) + // Task { + // try await DataSourceFacade.responseToUserFollowAction( + // dependency: self, + // user: record + // ) + // } + // case .muting: + // case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: + // break + // } + } + func profileHeaderViewController( _ profileHeaderViewController: ProfileHeaderViewController, profileHeaderView: ProfileHeaderView, diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index 5a6ec366f..77935cb9a 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -108,7 +108,7 @@ extension ReportResultViewController { guard !self.viewModel.isRequestMute else { return } self.viewModel.isRequestMute = true do { - try await DataSourceFacade.responseToUserMuteAction( + _ = try await DataSourceFacade.responseToUserMuteAction( dependency: self, account: self.viewModel.account ) @@ -128,9 +128,9 @@ extension ReportResultViewController { guard !self.viewModel.isRequestBlock else { return } self.viewModel.isRequestBlock = true do { - try await DataSourceFacade.responseToUserBlockAction( + _ = try await DataSourceFacade.responseToUserBlockAction( dependency: self, - user: self.viewModel.account + account: self.viewModel.account ) } catch { // handle error diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift index a36863458..ae6ea3f0e 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift @@ -147,7 +147,7 @@ extension Persistence.MastodonUser { let me = context.me user.update(isFollowing: relationship.following, by: me) - relationship.requested.flatMap { user.update(isFollowRequested: $0, by: me) } + user.update(isFollowRequested: relationship.requested, by: me) // relationship.endorsed.flatMap { user.update(isEndorsed: $0, by: me) } me.update(isFollowing: relationship.followedBy, by: user) user.update(isMuting: relationship.muting, by: me) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 8d70ead23..9640500ac 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -64,51 +64,7 @@ extension APIService { account: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - - guard let me = authenticationBox.authentication.account(), - let relationship = try await relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first - else { throw APIError.implicit(.badRequest) } - - let blockContext = MastodonBlockContext( - sourceUserID: me.id, - targetUserID: account.id, - targetUsername: account.username, - isBlocking: relationship.blocking, - isFollowing: relationship.following - ) - - let result: Result, Error> - do { - if blockContext.isBlocking { - let response = try await Mastodon.API.Account.unblock( - session: session, - domain: authenticationBox.domain, - accountID: blockContext.targetUserID, - authorization: authenticationBox.userAuthorization - ).singleOutput() - result = .success(response) - } else { - let response = try await Mastodon.API.Account.block( - session: session, - domain: authenticationBox.domain, - accountID: blockContext.targetUserID, - authorization: authenticationBox.userAuthorization - ).singleOutput() - result = .success(response) - } - } catch { - result = .failure(error) - } - - let response = try result.get() - return response - } - - public func toggleBlock( - user: Mastodon.Entity.Account, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - guard let relationship = try await relationship(forAccounts: [user], authenticationBox: authenticationBox).value.first else { + guard let relationship = try await relationship(forAccounts: [account], authenticationBox: authenticationBox).value.first else { throw APIError.implicit(.badRequest) } @@ -118,14 +74,14 @@ extension APIService { response = try await Mastodon.API.Account.unblock( session: session, domain: authenticationBox.domain, - accountID: user.id, + accountID: account.id, authorization: authenticationBox.userAuthorization ).singleOutput() } else { response = try await Mastodon.API.Account.block( session: session, domain: authenticationBox.domain, - accountID: user.id, + accountID: account.id, authorization: authenticationBox.userAuthorization ).singleOutput() } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift index bd02f9070..ebda38749 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Relationship.swift @@ -22,7 +22,7 @@ extension Mastodon.Entity { /// Are you following this user? public let following: Bool /// Do you have a pending follow request for this user? - public let requested: Bool? + public let requested: Bool /// Are you featuring this user on your profile? public let endorsed: Bool /// Are you followed by this user? diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift index efc0a8f1f..892a82b0b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift @@ -87,7 +87,7 @@ extension ProfileRelationshipActionButton { setTitle(title, for: .normal) - if relationship.blocking || account.suspended ?? false { + if relationship.blockedBy || account.suspended ?? false { isEnabled = false } else { isEnabled = true From 2a14e293e975f88e697f9eac373b7bd0721b0b28 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 22:37:45 +0100 Subject: [PATCH 078/159] Move directory-stuff into its own FileManager-file (IOS-192) --- .../Persistence/FileManager+SearchHistory.swift | 10 ---------- .../Persistence/FileManager+Shared.swift | 13 +++++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift index e289044d3..91c689f6b 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift @@ -63,13 +63,3 @@ extension FileManager { storeJSON(newSearchItems, .searchHistory) } } - -public extension FileManager { - var documentsDirectory: URL? { - urls(for: .documentDirectory, in: .userDomainMask).first - } - - var cachesDirectory: URL? { - urls(for: .cachesDirectory, in: .userDomainMask).first - } -} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift new file mode 100644 index 000000000..366ed3cb9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation + +public extension FileManager { + var documentsDirectory: URL? { + urls(for: .documentDirectory, in: .userDomainMask).first + } + + var cachesDirectory: URL? { + urls(for: .cachesDirectory, in: .userDomainMask).first + } +} From 460ede4852046820dd4e403efc4df14976ab0a51 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 22:39:24 +0100 Subject: [PATCH 079/159] User UserIdentification for search and accounts (IOS-192) Thanks to @kimar! --- .../DataSourceFacade+SearchHistory.swift | 4 +-- .../Login/MastodonLoginViewController.swift | 33 +++++++----------- .../SearchHistoryViewController.swift | 10 ++---- .../SearchHistoryViewModel.swift | 2 +- .../AuthenticationServiceProvider.swift | 2 +- .../MastodonCore/MastodonAuthentication.swift | 10 +++++- .../Persistence/FileManager+Account.swift | 17 +++++----- .../FileManager+SearchHistory.swift | 34 ++++++++----------- .../Persistence/Persistence.swift | 14 ++++---- 9 files changed, 59 insertions(+), 67 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 5fd9c1681..b4b9613cb 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -27,7 +27,7 @@ extension DataSourceFacade { hashtag: nil ) - try? FileManager.default.addSearchItem(searchEntry) + try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) case .hashtag(let tag): let now = Date() @@ -39,7 +39,7 @@ extension DataSourceFacade { hashtag: tag ) - try? FileManager.default.addSearchItem(searchEntry) + try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) case .status: break case .user(_): diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 97d26a968..3d51393b9 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -139,30 +139,23 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { @objc func login() { guard let server = viewModel.selectedServer else { return } - + authenticationViewModel - .authenticated - .asyncMap { domain, user -> Result in - do { - let result = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id) - return .success(user) - } catch { - return .failure(error) - } - } - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - switch result { - case .failure(let error): - assertionFailure(error.localizedDescription) - case .success(let account): - FileManager.default.store(account: account, forUserID: account.id) - self.coordinator.setup() + .authenticated.sink { (domain, account) in + Task { + do { + _ = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: account.id) + FileManager.default.store(account: account, forUserID: MastodonUserIdentifier(domain: domain, userID: account.id)) + Task { @MainActor in + self.coordinator.setup() + } + } catch { + assertionFailure(error.localizedDescription) + } } } .store(in: &disposeBag) - + authenticationViewModel.isAuthenticating.send(true) context.apiService.createApplication(domain: server.domain) .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index afddfbbac..a01100be7 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -50,8 +50,7 @@ extension SearchHistoryViewController { } override func viewWillAppear(_ animated: Bool) { - let userID = authContext.mastodonAuthenticationBox.userID - viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? [] + viewModel.items = (try? FileManager.default.searchItems(for: authContext.mastodonAuthenticationBox)) ?? [] } } @@ -103,9 +102,7 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa _ searchHistorySectionHeaderCollectionReusableView: SearchHistorySectionHeaderCollectionReusableView, clearButtonDidPressed button: UIButton ) { - let userID = authContext.mastodonAuthenticationBox.userID - - FileManager.default.removeSearchHistory(forUser: userID) + FileManager.default.removeSearchHistory(for: authContext.mastodonAuthenticationBox) viewModel.items = [] } } @@ -113,7 +110,6 @@ extension SearchHistoryViewController: SearchHistorySectionHeaderCollectionReusa //MARK: - SearchResultOverviewCoordinatorDelegate extension SearchHistoryViewController: SearchResultOverviewCoordinatorDelegate { func newSearchHistoryItemAdded(_ coordinator: SearchResultOverviewCoordinator) { - let userID = authContext.mastodonAuthenticationBox.userID - viewModel.items = (try? FileManager.default.searchItems(forUser: userID)) ?? [] + viewModel.items = (try? FileManager.default.searchItems(for: authContext.mastodonAuthenticationBox)) ?? [] } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift index 63f1cd0e8..1a1e38365 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift @@ -24,7 +24,7 @@ final class SearchHistoryViewModel { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.items = (try? FileManager.default.searchItems(forUser: authContext.mastodonAuthenticationBox.userID)) ?? [] + self.items = (try? FileManager.default.searchItems(for: authContext.mastodonAuthenticationBox)) ?? [] } } diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index 1f120e522..f5a9c5ea8 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -116,7 +116,7 @@ public extension AuthenticationServiceProvider { userID: authentication.userID, authorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)).value else { continue } - FileManager.default.store(account: account, forUserID: authentication.userID) + FileManager.default.store(account: account, forUserID: authentication.userIdentifier()) } NotificationCenter.default.post(name: .userFetched, object: nil) diff --git a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift index 317e9ad52..2ed7542df 100644 --- a/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift +++ b/MastodonSDK/Sources/MastodonCore/MastodonAuthentication.swift @@ -100,11 +100,19 @@ public struct MastodonAuthentication: Codable, Hashable { } public func account() -> Mastodon.Entity.Account? { - let account = FileManager.default.accounts(forUserID: userID).first(where: { $0.id == userID }) + + let account = FileManager + .default + .accounts(for: self.userIdentifier()) + .first(where: { $0.id == userID }) return account } + public func userIdentifier() -> MastodonUserIdentifier { + MastodonUserIdentifier(domain: domain, userID: userID) + } + func updating(instance: Instance) -> Self { copy(instanceObjectIdURI: instance.objectID.uriRepresentation()) } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift index 1df3aef1e..d9b21600a 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift @@ -3,10 +3,9 @@ import Foundation import MastodonSDK -extension FileManager { - public func store(account: Mastodon.Entity.Account, forUserID userID: String) { - // store accounts for each loged in user - var accounts = accounts(forUserID: userID) +public extension FileManager { + func store(account: Mastodon.Entity.Account, forUserID userID: UserIdentifier) { + var accounts = accounts(for: userID) if let index = accounts.firstIndex(of: account) { accounts.remove(at: index) @@ -17,10 +16,10 @@ extension FileManager { storeJSON(accounts, userID: userID) } - public func accounts(forUserID userID: String) -> [Mastodon.Entity.Account] { + func accounts(for userId: UserIdentifier) -> [Mastodon.Entity.Account] { guard let documentsDirectory else { return [] } - let accountPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory) + let accountPath = Persistence.accounts(userId).filepath(baseURL: documentsDirectory) guard let data = try? Data(contentsOf: accountPath) else { return [] } @@ -35,8 +34,10 @@ extension FileManager { } } +} - private func storeJSON(_ encodable: Encodable, userID: String) { +private extension FileManager { + private func storeJSON(_ encodable: Encodable, userID: UserIdentifier) { guard let documentsDirectory else { return } let jsonEncoder = JSONEncoder() @@ -44,7 +45,7 @@ extension FileManager { do { let data = try jsonEncoder.encode(encodable) - let accountsPath = Persistence.accounts(userID: userID).filepath(baseURL: documentsDirectory) + let accountsPath = Persistence.accounts( userID).filepath(baseURL: documentsDirectory) try data.write(to: accountsPath) } catch { debugPrint(error.localizedDescription) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift index 91c689f6b..a63515cb1 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+SearchHistory.swift @@ -1,17 +1,12 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import Foundation -import MastodonCore -extension FileManager { - public func searchItems(forUser userID: String) throws -> [Persistence.SearchHistory.Item] { - return try searchItems().filter { $0.userID == userID } - } - - public func searchItems() throws -> [Persistence.SearchHistory.Item] { +public extension FileManager { + func searchItems(for userId: UserIdentifier) throws -> [Persistence.SearchHistory.Item] { guard let documentsDirectory else { return [] } - let searchHistoryPath = Persistence.searchHistory.filepath(baseURL: documentsDirectory) + let searchHistoryPath = Persistence.searchHistory(userId).filepath(baseURL: documentsDirectory) guard let data = try? Data(contentsOf: searchHistoryPath) else { return [] } @@ -28,16 +23,23 @@ extension FileManager { } } - public func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item) throws { - var searchItems = (try? searchItems()) ?? [] + func addSearchItem(_ newSearchItem: Persistence.SearchHistory.Item, for userId: UserIdentifier) throws { + var searchItems = (try? searchItems(for: userId)) ?? [] if let index = searchItems.firstIndex(of: newSearchItem) { searchItems.remove(at: index) } - + searchItems.append(newSearchItem) - storeJSON(searchItems, .searchHistory) + storeJSON(searchItems, .searchHistory(userId)) + } + + func removeSearchHistory(for userId: UserIdentifier) { + let searchItems = (try? searchItems(for: userId)) ?? [] + let newSearchItems = searchItems.filter { $0.userID != userId.userID } + + storeJSON(newSearchItems, .searchHistory(userId)) } private func storeJSON(_ encodable: Encodable, _ persistence: Persistence) { @@ -53,13 +55,5 @@ extension FileManager { } catch { debugPrint(error.localizedDescription) } - - } - - public func removeSearchHistory(forUser userID: String) { - let searchItems = (try? searchItems()) ?? [] - let newSearchItems = searchItems.filter { $0.userID != userID } - - storeJSON(newSearchItems, .searchHistory) } } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index ffb45a26f..4326b6e47 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -9,11 +9,11 @@ import Foundation public enum Persistence { - case searchHistory + case searchHistory(UserIdentifier) case homeTimeline(UserIdentifier) case notificationsMentions(UserIdentifier) case notificationsAll(UserIdentifier) - case accounts(userID: String) + case accounts(UserIdentifier) private func uniqueUserDomainIdentifier(for userIdentifier: UserIdentifier) -> String { "\(userIdentifier.userID)@\(userIdentifier.domain)" @@ -21,16 +21,16 @@ public enum Persistence { private var filename: String { switch self { - case .searchHistory: - return "search_history" // todo: @zeitschlag should this be user-scoped as well? + case .searchHistory(let userIdentifier): + return "search_history_\(uniqueUserDomainIdentifier(for: userIdentifier))" // todo: @zeitschlag should this be user-scoped as well? case let .homeTimeline(userIdentifier): - return "home_timeline_\(userIdentifier.uniqueUserDomainIdentifier)" + return "home_timeline_\(uniqueUserDomainIdentifier(for: userIdentifier))" case let .notificationsMentions(userIdentifier): return "notifications_mentions_\(userIdentifier.uniqueUserDomainIdentifier)" case let .notificationsAll(userIdentifier): return "notifications_all_\(uniqueUserDomainIdentifier(for: userIdentifier))" - case .accounts(let userID): - return "account_\(userID)" + case .accounts(let userIdentifier): + return "account_\(uniqueUserDomainIdentifier(for: userIdentifier))" } } From f241232972bbcadd0376f549352fef287ba11566 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 23:07:20 +0100 Subject: [PATCH 080/159] Remove user (IOS-192) --- Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift | 2 -- .../DataSourceProvider+NotificationTableViewCellDelegate.swift | 2 -- .../DataSourceProvider+StatusTableViewCellDelegate.swift | 2 -- .../Provider/DataSourceProvider+UITableViewDelegate.swift | 2 -- Mastodon/Protocol/Provider/DataSourceProvider.swift | 2 -- .../SearchResultViewController+DataSourceProvider.swift | 2 -- 6 files changed, 12 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index b4b9613cb..76b19fa10 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -42,8 +42,6 @@ extension DataSourceFacade { try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox) case .status: break - case .user(_): - break case .notification: break diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 029aec5d4..7f139428f 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -487,8 +487,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut target: .status, // remove reblog wrapper status: status ) - case .user(let user): - break case .account(let account, let relationship): await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) case .notification: diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 0edb9e0cd..9d75caf6d 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -693,8 +693,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte provider: self, account: account ) - case .user(_): - assertionFailure("TODO") case .notification: assertionFailure("TODO") case .hashtag(_): diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index bf3570dbc..5233236b7 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -30,8 +30,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid target: .status, // remove reblog wrapper status: status ) - case .user(let user): - break case .hashtag(let tag): await DataSourceFacade.coordinateToHashtagScene( provider: self, diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index 30adaeacb..6334870b8 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -13,8 +13,6 @@ import class CoreDataStack.Notification enum DataSourceItem: Hashable { case status(record: MastodonStatus) - @available(*, deprecated, message: "Use .account") - case user(record: ManagedObjectRecord) case hashtag(tag: Mastodon.Entity.Tag) case notification(record: MastodonNotification) case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index b2fc91781..b6441e86a 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -69,8 +69,6 @@ extension SearchResultViewController { target: .status, // remove reblog wrapper status: status ) - case .user(let user): - assertionFailure() case .hashtag(let tag): await DataSourceFacade.coordinateToHashtagScene( provider: self, From c0e08c4482d44e3c03a9996ba38fa7bda2c170d6 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 23:07:38 +0100 Subject: [PATCH 081/159] Fix warnings (IOS-192) --- Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift | 4 ++-- .../Profile/Header/View/ProfileHeaderView+ViewModel.swift | 2 +- Mastodon/Scene/Profile/ProfileViewController.swift | 4 ++-- .../SearchResultViewController+DataSourceProvider.swift | 2 +- .../Sources/MastodonCore/Service/API/APIService+Follow.swift | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift b/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift index 6dcdc6f49..e2d92c98e 100644 --- a/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift +++ b/Mastodon/Scene/Discovery/ForYou/ProfileCardView.swift @@ -316,9 +316,9 @@ extension ProfileCardView { buttonState = .none } else if relationship.following { buttonState = .unfollow - } else if relationship.blocking || (relationship.domainBlocking ?? false) { + } else if relationship.blocking || relationship.domainBlocking { buttonState = .blocked - } else if relationship.requested ?? false { + } else if relationship.requested { buttonState = .pending } else { buttonState = .follow diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index b87928600..25e7347f1 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -133,7 +133,7 @@ extension ProfileHeaderView.ViewModel { .compactMap { relationship in guard let relationship else { return false } - return relationship.blocking || (relationship.blockedBy ?? false) + return relationship.blocking || relationship.blockedBy } .sink { needsImageOverlayBlurred in UIView.animate(withDuration: 0.33) { diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 1e4dc12d0..baff03943 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -420,14 +420,14 @@ extension ProfileViewController { let name = user.displayNameWithFallback var menuActions: [MastodonMenu.Action] = [ - .muteUser(.init(name: name, isMuting: relationship.muting ?? false)), + .muteUser(.init(name: name, isMuting: relationship.muting)), .blockUser(.init(name: name, isBlocking: relationship.blocking)), .reportUser(.init(name: name)), .shareUser(.init(name: name)), ] if relationship.following { - let showReblogs = relationship.showingReblogs ?? false// me.showingReblogsBy.contains(user) + let showReblogs = relationship.showingReblogs// me.showingReblogsBy.contains(user) let context = MastodonMenu.HideReblogsActionContext(showReblogs: showReblogs) menuActions.insert(.hideReblogs(context), at: 1) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index b6441e86a..b4e8b0c2e 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -62,7 +62,7 @@ extension SearchResultViewController { 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, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index f6a5a572f..cae26cf3f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -40,7 +40,7 @@ extension APIService { let response: Mastodon.Response.Content - if relationship.following || (relationship.requested ?? false) { + if relationship.following || relationship.requested { // unfollow response = try await Mastodon.API.Account.unfollow( session: session, From 11235f3f75b9118d775bb04b60091a2acb41cdc2 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 23:32:20 +0100 Subject: [PATCH 082/159] Re-enable pending-check (IOS-192) --- .../View/ProfileHeaderView+ViewModel.swift | 9 +-- Mastodon/Scene/Profile/ProfileViewModel.swift | 73 ++++++++----------- .../SuggestionAccountViewController.swift | 4 +- 3 files changed, 34 insertions(+), 52 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 25e7347f1..2ad403a82 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -268,7 +268,7 @@ extension ProfileHeaderView.ViewModel { $isRelationshipActionButtonHidden .assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer) .store(in: &disposeBag) -#warning("TODO: Implement") + Publishers.CombineLatest3( Publishers.CombineLatest3($me, $account, $relationship).eraseToAnyPublisher(), $isEditing, @@ -280,13 +280,6 @@ extension ProfileHeaderView.ViewModel { guard let relationship else { return } view.relationshipActionButton.configure(relationship: relationship, between: account, and: me, isEditing: isEditing, isUpdating: isUpdating) -// if relationshipActionOptionSet.contains(.edit) { -// // check .edit state and set .editing when isEditing -// view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit)) -// view.configure(state: isEditing ? .editing : .normal) -// } else { -// view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet) -// } } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 68e13bde0..a72846451 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -124,37 +124,38 @@ class ProfileViewModel: NSObject { } .store(in: &disposeBag) // query relationship - #warning("TODO: Implement") -// let pendingRetryPublisher = CurrentValueSubject(1) -// // observe friendship -// Publishers.CombineLatest( -// account, -// pendingRetryPublisher -// ) -// .sink { [weak self] account, _ in -// guard let self, let account else { return } -// -// Task { -// do { -// let response = try await self.updateRelationship( -// account: account, -// authenticationBox: self.authContext.mastodonAuthenticationBox -// ) -// // there are seconds delay after request follow before requested -> following. Query again when needs -// guard let relationship = response.value.first else { return } -// if relationship.requested == true { -// let delay = pendingRetryPublisher.value -// DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in -// guard let _ = self else { return } -// pendingRetryPublisher.value = min(2 * delay, 60) -// } -// } -// } catch { -// } -// } // end Task -// } -// .store(in: &disposeBag) + let pendingRetryPublisher = CurrentValueSubject(1) + + // observe friendship + Publishers.CombineLatest( + $account, + pendingRetryPublisher + ) + .sink { [weak self] account, _ in + guard let self else { return } + + Task { + do { + let response = try await self.context.apiService.relationship( + forAccounts: [account], + authenticationBox: self.authContext.mastodonAuthenticationBox + ) + + // there are seconds delay after request follow before requested -> following. Query again when needs + guard let relationship = response.value.first else { return } + if relationship.requested == true { + let delay = pendingRetryPublisher.value + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let _ = self else { return } + pendingRetryPublisher.value = min(2 * delay, 60) + } + } + } catch { + } + } // end Task + } + .store(in: &disposeBag) let isBlockingOrBlocked = Publishers.CombineLatest3( (relationship?.blocking ?? false).publisher, @@ -182,18 +183,6 @@ class ProfileViewModel: NSObject { let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) return context.apiService.accountVerifyCredentials(domain: domain, authorization: authorization) } - - private func updateRelationship( - account: Mastodon.Entity.Account, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { - let response = try await context.apiService.relationship( - forAccounts: [account], - authenticationBox: authenticationBox - ) - return response - } - } extension ProfileViewModel { diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 84d4f9e23..3962feff7 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -90,8 +90,8 @@ extension SuggestionAccountViewController: UITableViewDelegate { guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return } guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { - case .account(let account, _): - Task { await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) } + case .account(let account, _): + DataSourceFacade.coordinateToProfileScene(provider: self, account: account) } tableView.deselectRow(at: indexPath, animated: true) From 71b28cb9779fbe3e9731b29235346326f778e607 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 23:43:27 +0100 Subject: [PATCH 083/159] We don't use the author when composing toots (IOS-192) --- .../ComposeContent/ComposeContentViewModel.swift | 11 ++--------- .../Publisher/MastodonStatusPublisher.swift | 7 ------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 5539f80ee..9ee1cab64 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -552,14 +552,8 @@ extension ComposeContentViewModel { public func statusPublisher() throws -> StatusPublisher { let authContext = self.authContext - - // author - let managedObjectContext = self.context.managedObjectContext - var _author: ManagedObjectRecord? - managedObjectContext.performAndWait { - _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord - } - guard let author = _author else { + + guard authContext.mastodonAuthenticationBox.authentication.account() != nil else { throw AppError.badAuthentication } @@ -582,7 +576,6 @@ extension ComposeContentViewModel { } return MastodonStatusPublisher( - author: author, replyTo: { if case .reply(let status) = destination { return status diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index 9db9faed9..1c519a857 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -13,11 +13,6 @@ import MastodonCore import MastodonSDK public final class MastodonStatusPublisher: NSObject, ProgressReporting { - - // Input - - // author - public let author: ManagedObjectRecord // refer public let replyTo: MastodonStatus? // content warning @@ -47,7 +42,6 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { public var reactor: StatusPublisherReactor? public init( - author: ManagedObjectRecord, replyTo: MastodonStatus?, isContentWarningComposing: Bool, contentWarning: String, @@ -61,7 +55,6 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { visibility: Mastodon.Entity.Status.Visibility, language: String ) { - self.author = author self.replyTo = replyTo self.isContentWarningComposing = isContentWarningComposing self.contentWarning = contentWarning From 77f0f28960b8075e2d2e8bb902adc125ae045f16 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 23:45:30 +0100 Subject: [PATCH 084/159] Don't persist follow-requests (IOS-192) --- .../API/APIService+FollowRequest.swift | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift index f90aab5d3..60d5d1837 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+FollowRequest.swift @@ -26,27 +26,6 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate( - domain: authenticationBox.domain, - id: authenticationBox.userID - ) - request.fetchLimit = 1 - guard let user = managedObjectContext.safeFetch(request).first else { return } - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - - Persistence.MastodonUser.update( - mastodonUser: user, - context: Persistence.MastodonUser.RelationshipContext( - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - } - return response } From bb3ad7795482bcd317e8c6b666112d731113ec63 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 28 Dec 2023 23:50:40 +0100 Subject: [PATCH 085/159] Remove persistence for tags (IOS-192) Was connected to `let me = authenticationBox.authentication.user(in: managedObjectContext)` --- .../Persistence/Persistence+Tag.swift | 135 ------------------ .../Persistence/Persistence.swift | 1 - .../Service/API/APIService+Tags.swift | 34 +---- 3 files changed, 4 insertions(+), 166 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Tag.swift diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Tag.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Tag.swift deleted file mode 100644 index 163019505..000000000 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Tag.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// Persistence+Tag.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// - -import CoreData -import CoreDataStack -import Foundation -import MastodonSDK - -extension Persistence.Tag { - - public struct PersistContext { - public let domain: String - public let entity: Mastodon.Entity.Tag - public let me: MastodonUser? - public let networkDate: Date - - public init( - domain: String, - entity: Mastodon.Entity.Tag, - me: MastodonUser?, - networkDate: Date - ) { - self.domain = domain - self.entity = entity - self.me = me - self.networkDate = networkDate - } - } - - public struct PersistResult { - public let tag: Tag - public let isNewInsertion: Bool - - public init( - tag: Tag, - isNewInsertion: Bool - ) { - self.tag = tag - self.isNewInsertion = isNewInsertion - } - } - - public static func createOrMerge( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> PersistResult { - if let old = fetch(in: managedObjectContext, context: context) { - merge(tag: old, context: context) - return PersistResult( - tag: old, - isNewInsertion: false - ) - } else { - let object = create( - in: managedObjectContext, - context: context - ) - - return PersistResult( - tag: object, - isNewInsertion: false - ) - } - } - -} - -extension Persistence.Tag { - - public static func fetch( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> Tag? { - let request = Tag.sortedFetchRequest - request.predicate = Tag.predicate(domain: context.domain, name: context.entity.name) - request.fetchLimit = 1 - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - } - - @discardableResult - public static func create( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> Tag { - let property = Tag.Property( - entity: context.entity, - domain: context.domain, - networkDate: context.networkDate - ) - let object = Tag.insert( - into: managedObjectContext, - property: property - ) - update(tag: object, context: context) - if let followingUser = context.me { - object.update(followed: property.following, by: followingUser) - } - return object - } - - public static func merge( - tag: Tag, - context: PersistContext - ) { - guard context.networkDate > tag.updatedAt else { return } - let property = Tag.Property( - entity: context.entity, - domain: context.domain, - networkDate: context.networkDate - ) - - tag.update(property: property) - if let followingUser = context.me { - tag.update(followed: property.following, by: followingUser) - } - update(tag: tag, context: context) - } - - private static func update( - tag: Tag, - context: PersistContext - ) { - tag.update(updatedAt: context.networkDate) - } - -} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index 4326b6e47..b933ca2ae 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -48,7 +48,6 @@ extension Persistence { public enum Poll { } public enum Card { } public enum PollOption { } - public enum Tag { } public enum SearchHistory { } public enum Notification { } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift index 008a7e44e..bb9961f18 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Tags.swift @@ -27,8 +27,8 @@ extension APIService { authorization: authorization ).singleOutput() - return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) - } // end func + return response + } public func followTag( for tag: String, @@ -44,8 +44,8 @@ extension APIService { authorization: authorization ).singleOutput() - return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) - } // end func + return response + } public func unfollowTag( for tag: String, @@ -61,32 +61,6 @@ extension APIService { authorization: authorization ).singleOutput() - return try await persistTag(from: response, domain: domain, authenticationBox: authenticationBox) - } // end func -} - -fileprivate extension APIService { - @available(*, deprecated, message: "We don't persist tags anymore") - func persistTag( - from response: Mastodon.Response.Content, - domain: String, - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content { - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - - _ = Persistence.Tag.createOrMerge( - in: managedObjectContext, - context: Persistence.Tag.PersistContext( - domain: domain, - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - } - return response } } From 47986262bc8972f55799c4805905a42e2e75940f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 29 Dec 2023 18:06:48 +0100 Subject: [PATCH 086/159] Use relationships in Report-process (IOS-192) --- Mastodon/Coordinator/SceneCoordinator.swift | 4 +- .../Provider/DataSourceFacade+Status.swift | 3 + .../Report/Report/ReportViewController.swift | 26 +- .../Scene/Report/Report/ReportViewModel.swift | 3 + .../ReportReasonViewController.swift | 16 +- .../ReportResult/ReportResultView.swift | 6 +- .../ReportResultViewController.swift | 9 +- .../ReportResult/ReportResultViewModel.swift | 6 +- .../ProfileRelationshipActionButton.swift | 19 -- .../ViewModel/RelationshipViewModel.swift | 269 ------------------ 10 files changed, 43 insertions(+), 318 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 8f1c9cc3d..ef7f3b9cf 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -502,9 +502,7 @@ private extension SceneCoordinator { _viewController.viewModel = viewModel viewController = _viewController case .report(let viewModel): - let _viewController = ReportViewController() - _viewController.viewModel = viewModel - viewController = _viewController + viewController = ReportViewController(viewModel: viewModel) case .reportServerRules(let viewModel): let _viewController = ReportServerRulesViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 8ab436994..2595da4dc 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -244,10 +244,13 @@ extension DataSourceFacade { case .reportUser: Task { + guard let relationship = try? await dependency.context.apiService.relationship(forAccounts: [menuContext.author], authenticationBox: dependency.authContext.mastodonAuthenticationBox).value.first else { return } + let reportViewModel = ReportViewModel( context: dependency.context, authContext: dependency.authContext, account: menuContext.author, + relationship: relationship, status: menuContext.statusViewModel?.originalStatus ) diff --git a/Mastodon/Scene/Report/Report/ReportViewController.swift b/Mastodon/Scene/Report/Report/ReportViewController.swift index ae4c3448f..67b8046a8 100644 --- a/Mastodon/Scene/Report/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/Report/ReportViewController.swift @@ -20,19 +20,22 @@ class ReportViewController: UIViewController, NeedsDependency, ReportViewControl weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - var viewModel: ReportViewModel! - + let viewModel: ReportViewModel + lazy var cancelBarButtonItem = UIBarButtonItem( barButtonSystemItem: .cancel, target: self, action: #selector(ReportViewController.cancelBarButtonItemDidPressed(_:)) ) - -} + init(viewModel: ReportViewModel) { + self.viewModel = viewModel -extension ReportViewController { + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func viewDidLoad() { super.viewDidLoad() @@ -46,11 +49,10 @@ extension ReportViewController { viewModel.reportStatusViewModel.delegate = self viewModel.reportSupplementaryViewModel.delegate = self - let reportReasonViewController = ReportReasonViewController() + let reportReasonViewController = ReportReasonViewController(viewModel: viewModel.reportReasonViewModel) reportReasonViewController.context = context reportReasonViewController.coordinator = coordinator - reportReasonViewController.viewModel = viewModel.reportReasonViewModel - + addChild(reportReasonViewController) reportReasonViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(reportReasonViewController.view) @@ -58,10 +60,6 @@ extension ReportViewController { reportReasonViewController.view.pinToParent() } -} - -extension ReportViewController { - @objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) { dismiss(animated: true, completion: nil) } @@ -85,6 +83,7 @@ extension ReportViewController: ReportReasonViewControllerDelegate { context: context, authContext: viewModel.authContext, account: viewModel.account, + relationship: viewModel.relationship, isReported: false ) _ = coordinator.present( @@ -156,11 +155,12 @@ extension ReportViewController: ReportSupplementaryViewControllerDelegate { Task { @MainActor in do { let _ = try await viewModel.report() - + let reportResultViewModel = ReportResultViewModel( context: context, authContext: viewModel.authContext, account: viewModel.account, + relationship: viewModel.relationship, isReported: true ) diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index 1a21885f9..cff16063a 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -29,6 +29,7 @@ class ReportViewModel { let context: AppContext let authContext: AuthContext let account: Mastodon.Entity.Account + let relationship: Mastodon.Entity.Relationship let status: MastodonStatus? // output @@ -40,11 +41,13 @@ class ReportViewModel { context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, + relationship: Mastodon.Entity.Relationship, status: MastodonStatus? ) { self.context = context self.authContext = authContext self.account = account + self.relationship = relationship self.status = status self.reportReasonViewModel = ReportReasonViewModel(context: context) self.reportServerRulesViewModel = ReportServerRulesViewModel(context: context) diff --git a/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift b/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift index 2bfb689d0..2e89229ca 100644 --- a/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift +++ b/Mastodon/Scene/Report/ReportReason/ReportReasonViewController.swift @@ -25,9 +25,9 @@ final class ReportReasonViewController: UIViewController, NeedsDependency, Repor var disposeBag = Set() private var observations = Set() - var viewModel: ReportReasonViewModel! - private(set) lazy var reportReasonView = ReportReasonView(viewModel: viewModel) - + let viewModel: ReportReasonViewModel + let reportReasonView: ReportReasonView + let navigationActionView: NavigationActionView = { let navigationActionView = NavigationActionView() navigationActionView.backgroundColor = Asset.Scene.Onboarding.background.color @@ -35,10 +35,14 @@ final class ReportReasonViewController: UIViewController, NeedsDependency, Repor return navigationActionView }() + init(viewModel: ReportReasonViewModel) { + self.viewModel = viewModel + reportReasonView = ReportReasonView(viewModel: viewModel) + + super.init(nibName: nil, bundle: nil) + } -} - -extension ReportReasonViewController { + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift index 361f5db24..0805cb315 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift @@ -75,7 +75,7 @@ struct ReportResultView: View { action: { viewModel.followActionPublisher.send() }, - title: viewModel.relationshipViewModel.isFollowing ? L10n.Scene.Report.StepFinal.unfollow : L10n.Scene.Report.StepFinal.unfollowed, + title: viewModel.relationship.following ? L10n.Scene.Report.StepFinal.unfollow : L10n.Scene.Report.StepFinal.unfollowed, isBusy: viewModel.isRequestFollow ) } @@ -92,7 +92,7 @@ struct ReportResultView: View { action: { viewModel.muteActionPublisher.send() }, - title: viewModel.relationshipViewModel.isMuting ? L10n.Common.Controls.Friendship.muted : L10n.Common.Controls.Friendship.mute, + title: viewModel.relationship.muting ? L10n.Common.Controls.Friendship.muted : L10n.Common.Controls.Friendship.mute, isBusy: viewModel.isRequestMute ) } @@ -109,7 +109,7 @@ struct ReportResultView: View { action: { viewModel.blockActionPublisher.send() }, - title: viewModel.relationshipViewModel.isBlocking ? L10n.Common.Controls.Friendship.blocked : L10n.Common.Controls.Friendship.block, + title: viewModel.relationship.blocking ? L10n.Common.Controls.Friendship.blocked : L10n.Common.Controls.Friendship.block, isBusy: viewModel.isRequestBlock ) } diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift index 77935cb9a..62b82e8a1 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewController.swift @@ -88,10 +88,11 @@ extension ReportResultViewController { guard !self.viewModel.isRequestFollow else { return } self.viewModel.isRequestFollow = true do { - try await DataSourceFacade.responseToUserFollowAction( + let newRelationship = try await DataSourceFacade.responseToUserFollowAction( dependency: self, account: self.viewModel.account ) + self.viewModel.relationship = newRelationship } catch { // handle error } @@ -108,10 +109,11 @@ extension ReportResultViewController { guard !self.viewModel.isRequestMute else { return } self.viewModel.isRequestMute = true do { - _ = try await DataSourceFacade.responseToUserMuteAction( + let newRelationship = try await DataSourceFacade.responseToUserMuteAction( dependency: self, account: self.viewModel.account ) + self.viewModel.relationship = newRelationship } catch { // handle error } @@ -128,10 +130,11 @@ extension ReportResultViewController { guard !self.viewModel.isRequestBlock else { return } self.viewModel.isRequestBlock = true do { - _ = try await DataSourceFacade.responseToUserBlockAction( + let newRelationship = try await DataSourceFacade.responseToUserBlockAction( dependency: self, account: self.viewModel.account ) + self.viewModel.relationship = newRelationship } catch { // handle error } diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index 70853e184..c7688963b 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -24,6 +24,7 @@ class ReportResultViewModel: ObservableObject { let context: AppContext let authContext: AuthContext let account: Mastodon.Entity.Account + var relationship: Mastodon.Entity.Relationship let isReported: Bool var headline: String { @@ -39,8 +40,7 @@ class ReportResultViewModel: ObservableObject { // output @Published var avatarURL: URL? @Published var username: String = "" - - let relationshipViewModel = RelationshipViewModel() + let muteActionPublisher = PassthroughSubject() let followActionPublisher = PassthroughSubject() let blockActionPublisher = PassthroughSubject() @@ -49,11 +49,13 @@ class ReportResultViewModel: ObservableObject { context: AppContext, authContext: AuthContext, account: Mastodon.Entity.Account, + relationship: Mastodon.Entity.Relationship, isReported: Bool ) { self.context = context self.authContext = authContext self.account = account + self.relationship = relationship self.isReported = isReported // end init diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift index 892a82b0b..eaf484dae 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift @@ -94,25 +94,6 @@ extension ProfileRelationshipActionButton { } } - public func configure(actionOptionSet: RelationshipActionOptionSet) { - setTitle(actionOptionSet.title, for: .normal) - - configureAppearance() - - titleEdgeInsets = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) - - activityIndicatorView.stopAnimating() - - if let option = actionOptionSet.highPriorityAction(except: .editOptions), option == .blocked || option == .suspended { - isEnabled = false - } else if actionOptionSet.contains(.updating) { - isEnabled = false - activityIndicatorView.startAnimating() - } else { - isEnabled = true - } - } - private func configureAppearance() { setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) setTitleColor(Asset.Colors.Label.primaryReverse.color.withAlphaComponent(0.5), for: .highlighted) diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift deleted file mode 100644 index f24e1b47f..000000000 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// RelationshipViewModel.swift -// -// -// Created by MainasuK on 2022-4-14. -// - -import UIKit -import Combine -import MastodonAsset -import MastodonLocalization -import CoreDataStack - -public enum RelationshipAction: Int, CaseIterable { - case showReblogs - case isMyself - case followingBy - case blockingBy - case none // set hide from UI - case follow - case request - case pending - case following - case muting - case blocked - case blocking - case suspended - case edit - case editing - case updating - - public var option: RelationshipActionOptionSet { - return RelationshipActionOptionSet(rawValue: 1 << rawValue) - } -} - -// construct option set on the enum for safe iterator -public struct RelationshipActionOptionSet: OptionSet { - - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static let isMyself = RelationshipAction.isMyself.option - public static let followingBy = RelationshipAction.followingBy.option - public static let blockingBy = RelationshipAction.blockingBy.option - public static let none = RelationshipAction.none.option - public static let follow = RelationshipAction.follow.option - public static let request = RelationshipAction.request.option - public static let pending = RelationshipAction.pending.option - public static let following = RelationshipAction.following.option - public static let muting = RelationshipAction.muting.option - public static let blocked = RelationshipAction.blocked.option - public static let blocking = RelationshipAction.blocking.option - public static let suspended = RelationshipAction.suspended.option - public static let edit = RelationshipAction.edit.option - public static let editing = RelationshipAction.editing.option - public static let updating = RelationshipAction.updating.option - public static let showReblogs = RelationshipAction.showReblogs.option - public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating] - - public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? { - let set = subtracting(except) - for action in RelationshipAction.allCases.reversed() where set.contains(action.option) { - return action - } - - return nil - } - - public var title: String { - guard let highPriorityAction = self.highPriorityAction(except: []) else { - assertionFailure() - return " " - } - switch highPriorityAction { - case .isMyself: return "" - case .followingBy: return " " - case .blockingBy: return " " - case .none: return " " - case .follow: return L10n.Common.Controls.Friendship.follow - case .request: return L10n.Common.Controls.Friendship.request - case .pending: return L10n.Common.Controls.Friendship.pending - case .following: return L10n.Common.Controls.Friendship.following - case .muting: return L10n.Common.Controls.Friendship.muted - case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user (deprecated) - case .blocking: return L10n.Common.Controls.Friendship.blocked - case .suspended: return L10n.Common.Controls.Friendship.follow - case .edit: return L10n.Common.Controls.Friendship.editInfo - case .editing: return L10n.Common.Controls.Actions.done - case .updating: return " " - case .showReblogs: return " " - } - } -} - -@available(*, deprecated, message: "Replace with Mastodon.Entity.Relationship") -public final class RelationshipViewModel { - - var disposeBag = Set() - - public var userObserver: AnyCancellable? - public var meObserver: AnyCancellable? - - // input - @Published public var user: MastodonUser? - @Published public var me: MastodonUser? - public let relationshipUpdatePublisher = CurrentValueSubject(Void()) // needs initial event - - // output - @Published public var isMyself = false - @Published public var optionSet: RelationshipActionOptionSet? - - @Published public var isFollowing = false - @Published public var isFollowingBy = false - @Published public var isMuting = false - @Published public var showReblogs = false - @Published public var isBlocking = false - @Published public var isBlockingBy = false - @Published public var isSuspended = false - - public init() { - Publishers.CombineLatest3( - $user, - $me, - relationshipUpdatePublisher - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] user, me, _ in - guard let self = self else { return } - self.update(user: user, me: me) - - guard let user = user, let me = me else { - self.userObserver = nil - self.meObserver = nil - return - } - - // do not modify object to prevent infinity loop - self.userObserver = RelationshipViewModel.createObjectChangePublisher(user: user) - .sink { [weak self] _ in - guard let self = self else { return } - self.relationshipUpdatePublisher.send() - } - - self.meObserver = RelationshipViewModel.createObjectChangePublisher(user: me) - .sink { [weak self] _ in - guard let self = self else { return } - self.relationshipUpdatePublisher.send() - } - } - .store(in: &disposeBag) - } - -} - -extension RelationshipViewModel { - - public static func createObjectChangePublisher(user: MastodonUser) -> AnyPublisher { - return ManagedObjectObserver - .observe(object: user) - .map { _ in Void() } - .catch { error in - return Just(Void()) - } - .eraseToAnyPublisher() - } - -} - -extension RelationshipViewModel { - private func update(user: MastodonUser?, me: MastodonUser?) { - guard let user = user, - let me = me - else { - reset() - return - } - - let optionSet = RelationshipViewModel.optionSet(user: user, me: me) - - self.isMyself = optionSet.contains(.isMyself) - self.isFollowingBy = optionSet.contains(.followingBy) - self.isFollowing = optionSet.contains(.following) - self.isMuting = optionSet.contains(.muting) - self.isBlockingBy = optionSet.contains(.blockingBy) - self.isBlocking = optionSet.contains(.blocking) - self.isSuspended = optionSet.contains(.suspended) - self.showReblogs = optionSet.contains(.showReblogs) - - self.optionSet = optionSet - } - - private func reset() { - isMyself = false - isFollowingBy = false - isFollowing = false - isMuting = false - isBlockingBy = false - isBlocking = false - optionSet = nil - showReblogs = false - } -} - -extension RelationshipViewModel { - - public static func optionSet(user: MastodonUser, me: MastodonUser) -> RelationshipActionOptionSet { - let isMyself = user.id == me.id && user.domain == me.domain - guard !isMyself else { - return [.isMyself, .edit] - } - - let isProtected = user.locked - let isFollowingBy = me.followingBy.contains(user) - let isFollowing = user.followingBy.contains(me) - let isPending = user.followRequestedBy.contains(me) - let isMuting = user.mutingBy.contains(me) - let isBlockingBy = me.blockingBy.contains(user) - let isBlocking = user.blockingBy.contains(me) - let isShowingReblogs = me.showingReblogsBy.contains(user) - - var optionSet: RelationshipActionOptionSet = [.follow] - - if isMyself { - optionSet.insert(.isMyself) - } - - if isProtected { - optionSet.insert(.request) - } - - if isFollowingBy { - optionSet.insert(.followingBy) - } - - if isFollowing { - optionSet.insert(.following) - } - - if isPending { - optionSet.insert(.pending) - } - - if isMuting { - optionSet.insert(.muting) - } - - if isBlockingBy { - optionSet.insert(.blockingBy) - } - - if isBlocking { - optionSet.insert(.blocking) - } - - if user.suspended { - optionSet.insert(.suspended) - } - - if isShowingReblogs { - optionSet.insert(.showReblogs) - } - - return optionSet - } -} From 922f7116dd0403e88ed477cb26a71bc58780cd02 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 30 Dec 2023 10:36:10 +0100 Subject: [PATCH 087/159] Use account for reblogged statuses (stati?) (IOS-192) --- .../View/Content/StatusView+Configuration.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index ede5e8a15..b4238c80d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -88,16 +88,11 @@ extension StatusView { private func configureHeader(status: MastodonStatus) { if status.entity.reblogged == true, let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox, - let managedObjectContext = viewModel.context?.managedObjectContext { - - let user = MastodonUser.findOrFetch( - in: managedObjectContext, - matching: MastodonUser.predicate(domain: authenticationBox.domain, id: authenticationBox.userID) - ) + let account = authenticationBox.authentication.account() { + + let name = account.displayNameWithFallback + let emojis = account.emojis ?? [] - let name = user?.displayNameWithFallback ?? authenticationBox.authentication.username - let emojis = user?.emojis ?? [] - viewModel.header = { let text = L10n.Common.Controls.Status.userReblogged(name) let content = MastodonContent(content: text, emojis: emojis.asDictionary) From 0f6f6adfba73006d59ffd8ac4174219fd1e53b51 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 30 Dec 2023 12:04:39 +0100 Subject: [PATCH 088/159] Update header after edit (IOS-192) --- .../Profile/Header/ProfileHeaderViewController.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 6a8fceff9..4c8949e16 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -89,6 +89,15 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi self.profileHeaderView = ProfileHeaderView(account: profileViewModel.account, me: profileViewModel.me, relationship: profileViewModel.relationship) super.init(nibName: nil, bundle: nil) + + viewModel.$account + .receive(on: DispatchQueue.main) + .sink { [weak self] account in + guard let self else { return } + + self.profileHeaderView.configuration(account: account) + } + .store(in: &disposeBag) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -96,9 +105,6 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi override func viewDidLoad() { super.viewDidLoad() - profileHeaderView.prepareForReuse() - profileHeaderView.configuration(account: viewModel.account) - view.setContentHuggingPriority(.required - 1, for: .vertical) From 8e7e16870c665a0536dad34339cbde5cdbd12aa0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 30 Dec 2023 12:30:27 +0100 Subject: [PATCH 089/159] Fix some warnings (IOS-192) As relationship-information isn't optional anymore, there' no need for ?? everywhere (I just missed some) --- .../Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift | 2 +- MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift | 2 +- .../View/Control/ProfileRelationshipActionButton.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 2ad403a82..5ddb57dc4 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -204,7 +204,7 @@ extension ProfileHeaderView.ViewModel { guard let relationship else { return nil } let isBlocking = relationship.blocking - let isBlockedBy = relationship.blockedBy ?? false + let isBlockedBy = relationship.blockedBy let isSuspended = account.suspended ?? false let isNeedsHidden = isBlocking || isBlockedBy || isSuspended diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift index 5c8b66cad..59f6458a8 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -269,7 +269,7 @@ public extension UserView { buttonState = .unfollow } else if relationship.blocking || relationship.domainBlocking { buttonState = .blocked - } else if relationship.requested ?? false { + } else if relationship.requested { buttonState = .pending } else { buttonState = .follow diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift index eaf484dae..053bc72d4 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift @@ -73,7 +73,7 @@ extension ProfileRelationshipActionButton { } else if relationship.domainBlocking { #warning("Wait for #1198 (Domain Block, IOS-5) to be merged") title = "Unblock domain" - } else if (relationship.requested ?? false) { + } else if relationship.requested { title = L10n.Common.Controls.Friendship.pending } else if relationship.muting { title = L10n.Common.Controls.Friendship.muted From a2c03bd6d17bb4e1f33e8c7dbc01bee032533037 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 30 Dec 2023 12:30:44 +0100 Subject: [PATCH 090/159] Remove unused code (IOS-192) --- Mastodon.xcodeproj/project.pbxproj | 4 ---- ...estionAccountTableViewCell+ViewModel.swift | 22 ------------------- 2 files changed, 26 deletions(-) delete mode 100644 Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4c4c2db0c..b6a44d8fa 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -166,7 +166,6 @@ D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; }; D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */; }; D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; }; - D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */; }; D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; }; D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; }; D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; }; @@ -834,7 +833,6 @@ D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewModel.swift; sourceTree = ""; }; D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = ""; }; - D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = ""; }; D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = ""; }; D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = ""; }; D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = ""; }; @@ -1709,7 +1707,6 @@ isa = PBXGroup; children = ( 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */, - D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */, D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */, ); path = "TableView-Components"; @@ -3906,7 +3903,6 @@ DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, - D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */, D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, diff --git a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift deleted file mode 100644 index 1b4fcb34c..000000000 --- a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2023 Mastodon gGmbH. All rights reserved. - -import Combine -import MastodonUI -import CoreDataStack - -extension SuggestionAccountTableViewCell { - final class ViewModel { - let user: MastodonUser - - let followedUsers: [String] - let blockedUsers: [String] - let followRequestedUsers: [String] - - init(user: MastodonUser, followedUsers: [String], blockedUsers: [String], followRequestedUsers: [String]) { - self.user = user - self.followedUsers = followedUsers - self.followRequestedUsers = followRequestedUsers - self.blockedUsers = blockedUsers - } - } -} From c152c7b3c7cfa30425732e26d93ac4d94ab889cb Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 30 Dec 2023 12:54:13 +0100 Subject: [PATCH 091/159] Don't use user but account on Sidebar on iPad (IOS-192) --- .../Scene/Root/Sidebar/SidebarViewModel.swift | 86 ++++++++++--------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 441b8ca29..84b7f1cba 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -70,17 +70,18 @@ extension SidebarViewModel { secondaryCollectionView: UICollectionView ) { let tabCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in - guard let self = self else { return } - - let imageURL: URL? = { - switch item { + guard let self else { return } + + let imageURL: URL? + switch item { case .me: - let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) - return user?.avatarImageURL() - default: - return nil - } - }() + let account = self.authContext?.mastodonAuthenticationBox.authentication.account() + imageURL = account?.avatarImageURL() + case .home, .search, .compose, .notifications: + // no custom avatar for other tabs + imageURL = nil + } + cell.item = SidebarListContentView.Item( isActive: false, accessoryImage: item == .me ? self.chevronImage : nil, @@ -104,39 +105,40 @@ extension SidebarViewModel { .store(in: &cell.disposeBag) switch item { - case .notifications: - Publishers.CombineLatest( - self.context.notificationService.unreadNotificationCountDidUpdate, - self.$currentTab - ) - .receive(on: DispatchQueue.main) - .sink { [weak cell] authentication, currentTab in - guard let cell = cell else { return } - - let hasUnreadPushNotification: Bool = { - guard let accessToken = self.authContext?.mastodonAuthenticationBox.userAuthorization.accessToken else { return false } - let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) - return count > 0 - }() - - let image: UIImage - if hasUnreadPushNotification { - let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor]) - image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)! - } else { - image = MainTabBarController.Tab.notifications.image + case .notifications: + Publishers.CombineLatest( + self.context.notificationService.unreadNotificationCountDidUpdate, + self.$currentTab + ) + .receive(on: DispatchQueue.main) + .sink { [weak cell] authentication, currentTab in + guard let cell = cell else { return } + + let hasUnreadPushNotification: Bool = { + guard let accessToken = self.authContext?.mastodonAuthenticationBox.userAuthorization.accessToken else { return false } + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + return count > 0 + }() + + let image: UIImage + if hasUnreadPushNotification { + let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor]) + image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)! + } else { + image = MainTabBarController.Tab.notifications.image + } + cell.item?.image = image + cell.item?.activeImage = image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal) + cell.setNeedsUpdateConfiguration() } - cell.item?.image = image - cell.item?.activeImage = image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal) - cell.setNeedsUpdateConfiguration() - } - .store(in: &cell.disposeBag) - case .me: - guard let user = self.authContext?.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return } - let currentUserDisplayName = user.displayNameWithFallback - cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) - default: - break + .store(in: &cell.disposeBag) + case .me: + guard let account = self.authContext?.mastodonAuthenticationBox.authentication.account() else { return } + + let currentUserDisplayName = account.displayNameWithFallback + cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) + case .compose, .home, .search: + break } } From 4764116efcf1bd84061a2cb31bbe99be5acf24bd Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 1 Jan 2024 20:10:00 +0100 Subject: [PATCH 092/159] Use account to determine status-visibility (IOS-192) --- .../ComposeContent/ComposeContentViewModel.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 9ee1cab64..70f762ffa 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -156,7 +156,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.visibility = { // default private when user locked var visibility: Mastodon.Entity.Status.Visibility = { - guard let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) else { + guard let author = authContext.mastodonAuthenticationBox.authentication.account() else { return .public } return author.locked ? .private : .public @@ -196,7 +196,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { case .reply(let record): context.managedObjectContext.performAndWait { let status = record.entity - let author = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) + let author = authContext.mastodonAuthenticationBox.authentication.account() var mentionAccts: [String] = [] if author?.id != status.account.id { @@ -603,12 +603,7 @@ extension ComposeContentViewModel { guard case let .editStatus(status, _) = composeContext else { return nil } // author - let managedObjectContext = self.context.managedObjectContext - var _author: ManagedObjectRecord? - managedObjectContext.performAndWait { - _author = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext)?.asRecord - } - guard let author = _author else { + guard let author = authContext.mastodonAuthenticationBox.authentication.account() else { throw AppError.badAuthentication } From ab7b0584dd1ef066794898118e80607aac03a3df Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 1 Jan 2024 20:10:22 +0100 Subject: [PATCH 093/159] Replace more users with account (IOS-192) In Edit and notification --- .../Service/Notification/NotificationService.swift | 4 ++-- .../Publisher/MastodonStatusEditPublisher.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index 0c60ad8e7..b69b57ded 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -101,12 +101,12 @@ extension NotificationService { return try await managedObjectContext.perform { var items: [UIApplicationShortcutItem] = [] for authentication in AuthenticationServiceProvider.shared.authentications { - guard let user = authentication.user(in: managedObjectContext) else { continue } + guard let account = authentication.account() else { continue } let accessToken = authentication.userAccessToken let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) guard count > 0 else { continue } - let title = "@\(user.acctWithDomain)" + let title = "@\(account.acctWithDomain)" let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) let item = UIApplicationShortcutItem( diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift index 893fb1a28..59db01496 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift @@ -11,7 +11,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting { // Input public let statusID: Status.ID - public let author: ManagedObjectRecord + public let author: Mastodon.Entity.Account // content warning public let isContentWarningComposing: Bool @@ -41,7 +41,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting { public init( statusID: Status.ID, - author: ManagedObjectRecord, + author: Mastodon.Entity.Account, isContentWarningComposing: Bool, contentWarning: String, content: String, From a6aa4291f6b30cef4496d81fbc66d1f2f7436d9d Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 1 Jan 2024 20:10:37 +0100 Subject: [PATCH 094/159] Remove obsolete wrapper-code --- .../Service/API/APIService+Block.swift | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 9640500ac..c24a3b703 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -23,14 +23,8 @@ extension APIService { @discardableResult public func getBlocked( - authenticationBox: MastodonAuthenticationBox - ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { - try await _getBlocked(sinceID: nil, limit: nil, authenticationBox: authenticationBox) - } - - private func _getBlocked( - sinceID: Mastodon.Entity.Status.ID?, - limit: Int?, + sinceID: Mastodon.Entity.Status.ID? = nil, + limit: Int? = nil, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { let managedObjectContext = backgroundManagedObjectContext @@ -42,21 +36,6 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - let userIDs = response.value.map { $0.id } - let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs) - - let fetchRequest = MastodonUser.fetchRequest() - fetchRequest.predicate = predicate - fetchRequest.includesPropertyValues = false - - try await managedObjectContext.performChanges { - let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser] - - for user in users { - user.deleteStatusAndNotificationFeeds(in: managedObjectContext) - } - } - return response } From 2018d7c76479d3cbcc6f88520bdca53c473dc0c6 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 2 Jan 2024 11:05:58 +0100 Subject: [PATCH 095/159] Hide loading spinner after network requesty --- Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 82ec945fb..b1d39a4dd 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -34,10 +34,11 @@ extension DataSourceFacade { query: .init(acct: acct), authorization: provider.authContext.mastodonAuthenticationBox.userAuthorization ).singleOutput().value + + provider.coordinator.hideLoading() guard let redirectRecord = _redirectRecord else { assertionFailure() - provider.coordinator.hideLoading() return } await coordinateToProfileScene( From 54b020ff61daf67e55d43251b76eab43d5938c49 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 2 Jan 2024 11:32:27 +0100 Subject: [PATCH 096/159] Remove dead code --- .../MastodonUI/View/Content/MediaView+Configuration.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index 67372f544..f4e656210 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -191,7 +191,6 @@ extension MediaView { ) } -// let status: StatusCompatible = status.reblog ?? status let attachments = status.entity.mastodonAttachments let configurations = attachments.enumerated().map { (idx, attachment) -> MediaView.Configuration in let configuration: MediaView.Configuration = { From d1b5b9fc98e8b32dc43809cb5e9ff8c4a526814c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 2 Jan 2024 15:36:26 +0100 Subject: [PATCH 097/159] Localize button-title for blocked domains (IOS-192) --- .../View/Control/ProfileRelationshipActionButton.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift index 053bc72d4..5e9d098a4 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ProfileRelationshipActionButton.swift @@ -71,8 +71,7 @@ extension ProfileRelationshipActionButton { } else if relationship.blocking { title = L10n.Common.Controls.Friendship.blocked } else if relationship.domainBlocking { - #warning("Wait for #1198 (Domain Block, IOS-5) to be merged") - title = "Unblock domain" + title = L10n.Common.Controls.Friendship.domainBlocked } else if relationship.requested { title = L10n.Common.Controls.Friendship.pending } else if relationship.muting { From 7f26dfa6d6bd4484af506d70c3f183da1c3cd32a Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 2 Jan 2024 15:37:07 +0100 Subject: [PATCH 098/159] Fix warnings and remove unused code (IOS-192) --- .../Provider/DataSourceFacade+Hashtag.swift | 8 --- .../SearchHistoryViewController.swift | 2 +- .../Model/Compose/ComposeStatusItem.swift | 66 ------------------- 3 files changed, 1 insertion(+), 75 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift index b0a0c982e..8c4844166 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift @@ -11,14 +11,6 @@ import MastodonCore import MastodonSDK extension DataSourceFacade { - @MainActor - static func coordinateToHashtagScene( - provider: DataSourceProvider & AuthContextProvider, - tag: Mastodon.Entity.Tag - ) async { - await coordinateToHashtagScene(provider: provider, tag: tag) - } - @MainActor static func coordinateToHashtagScene( provider: ViewControllerWithDependencies & AuthContextProvider, diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index a01100be7..dd98bf463 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -75,7 +75,7 @@ extension SearchHistoryViewController: UICollectionViewDelegate { switch item { case .account(account: let account, relationship: _): - await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + DataSourceFacade.coordinateToProfileScene(provider: self, account: account) case .hashtag(let tag): await DataSourceFacade.coordinateToHashtagScene( diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift deleted file mode 100644 index 65650dcdc..000000000 --- a/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// ComposeStatusItem.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-11. -// - -import Foundation -import Combine -import CoreData -import MastodonMeta -import CoreDataStack - -/// Note: update Equatable when change case -enum ComposeStatusItem { - case replyTo(record: ManagedObjectRecord) - case input(replyTo: ManagedObjectRecord?, attribute: ComposeStatusAttribute) - case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute) - case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute) -} - -extension ComposeStatusItem: Hashable { } - -extension ComposeStatusItem { - final class ComposeStatusAttribute: Hashable { - private let id = UUID() - - @Published var author: ManagedObjectRecord? - - @Published var composeContent: String? - - @Published var isContentWarningComposing = false - @Published var contentWarningContent = "" - - static func == (lhs: ComposeStatusAttribute, rhs: ComposeStatusAttribute) -> Bool { - return lhs.author == rhs.author - && lhs.composeContent == rhs.composeContent - && lhs.isContentWarningComposing == rhs.isContentWarningComposing - && lhs.contentWarningContent == rhs.contentWarningContent - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - } -} - -extension ComposeStatusItem { - final class ComposeStatusAttachmentAttribute: Hashable { - private let id = UUID() - - var attachmentServices: [MastodonAttachmentService] - - init(attachmentServices: [MastodonAttachmentService]) { - self.attachmentServices = attachmentServices - } - - static func == (lhs: ComposeStatusAttachmentAttribute, rhs: ComposeStatusAttachmentAttribute) -> Bool { - return lhs.attachmentServices == rhs.attachmentServices - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - } -} From 45028373d44a6518ae7726a34b7650ba8939f4f3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 2 Jan 2024 23:05:25 +0100 Subject: [PATCH 099/159] Store accounts in container (IOS-192) In case you see an empty app: Add your account again. Background: As we need access to the account from the extensions and the extensions can't access the documents-directory but the group-container, well, the accounts will live there from now on. --- .../MastodonCore/Persistence/FileManager+Account.swift | 8 ++++---- .../MastodonCore/Persistence/FileManager+Shared.swift | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift index d9b21600a..cf72c53af 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Account.swift @@ -17,9 +17,9 @@ public extension FileManager { } func accounts(for userId: UserIdentifier) -> [Mastodon.Entity.Account] { - guard let documentsDirectory else { return [] } + guard let sharedDirectory else { return [] } - let accountPath = Persistence.accounts(userId).filepath(baseURL: documentsDirectory) + let accountPath = Persistence.accounts(userId).filepath(baseURL: sharedDirectory) guard let data = try? Data(contentsOf: accountPath) else { return [] } @@ -38,14 +38,14 @@ public extension FileManager { private extension FileManager { private func storeJSON(_ encodable: Encodable, userID: UserIdentifier) { - guard let documentsDirectory else { return } + guard let sharedDirectory else { return } let jsonEncoder = JSONEncoder() jsonEncoder.dateEncodingStrategy = .iso8601 do { let data = try jsonEncoder.encode(encodable) - let accountsPath = Persistence.accounts( userID).filepath(baseURL: documentsDirectory) + let accountsPath = Persistence.accounts( userID).filepath(baseURL: sharedDirectory) try data.write(to: accountsPath) } catch { debugPrint(error.localizedDescription) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift index 366ed3cb9..0afd30f2f 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Shared.swift @@ -1,6 +1,7 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import Foundation +import MastodonCommon public extension FileManager { var documentsDirectory: URL? { @@ -10,4 +11,8 @@ public extension FileManager { var cachesDirectory: URL? { urls(for: .cachesDirectory, in: .userDomainMask).first } + + var sharedDirectory: URL? { + containerURL(forSecurityApplicationGroupIdentifier: AppName.groupID) + } } From 5e2b2d3cdfbca56db13e63a6401fb675ca861bb0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 2 Jan 2024 23:09:50 +0100 Subject: [PATCH 100/159] Use accounts for widgets (IOS-192) --- .../Variants/FollowersCount/FollowersCountWidget.swift | 4 +--- .../MultiFollowersCount/MultiFollowersCountWidget.swift | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift index c50c58c9b..2c2c53ffd 100644 --- a/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift +++ b/WidgetExtension/Variants/FollowersCount/FollowersCountWidget.swift @@ -87,9 +87,7 @@ private extension FollowersCountWidgetProvider { } guard - let desiredAccount = configuration.account ?? authBox.authentication.user( - in: WidgetExtension.appContext.managedObjectContext - )?.acctWithDomain + let desiredAccount = configuration.account ?? authBox.authentication.account()?.acctWithDomain else { return completion(.unconfigured) } diff --git a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift index 38e3ddc8c..084a31f7c 100644 --- a/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift +++ b/WidgetExtension/Variants/MultiFollowersCount/MultiFollowersCountWidget.swift @@ -90,9 +90,7 @@ private extension MultiFollowersCountWidgetProvider { if let configuredAccounts = configuration.accounts?.compactMap({ $0 }) { desiredAccounts = configuredAccounts - } else if let currentlyLoggedInAccount = authBox.authentication.user( - in: WidgetExtension.appContext.managedObjectContext - )?.acctWithDomain { + } else if let currentlyLoggedInAccount = authBox.authentication.account()?.acctWithDomain { desiredAccounts = [currentlyLoggedInAccount] } else { return completion(.unconfigured) From 9a25d20e026bb11ad94053ef6348e5e37f39ddd0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 2 Jan 2024 23:10:17 +0100 Subject: [PATCH 101/159] Replace another user with an account (IOS-192) --- .../ComposeContentViewModel.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 70f762ffa..a70b35a42 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import Meta import MetaTextKit import MastodonMeta @@ -310,11 +309,19 @@ extension ComposeContentViewModel { // bind author $authContext .sink { [weak self] authContext in - guard let self = self else { return } - guard let user = authContext.mastodonAuthenticationBox.authentication.user(in: self.context.managedObjectContext) else { return } - self.avatarURL = user.avatarImageURL() - self.name = user.nameMetaContent ?? PlaintextMetaContent(string: user.displayNameWithFallback) - self.username = user.acctWithDomain + guard let self, let account = authContext.mastodonAuthenticationBox.authentication.account() else { return } + + self.avatarURL = account.avatarImageURL() + + do { + let content = MastodonContent(content: account.displayNameWithFallback, emojis: (account.emojis ?? []).asDictionary) + let metaContent = try MastodonMetaContent.convert(document: content) + self.name = metaContent + } catch { + self.name = PlaintextMetaContent(string: account.displayNameWithFallback) + } + + self.username = account.acctWithDomain } .store(in: &disposeBag) From c3d40d260d575cca3666b00f34ea8389fdcdd9a0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Jan 2024 11:29:25 +0100 Subject: [PATCH 102/159] Use accounts for shortcut-extension (IOS-192) This is broken atm, see #1204 --- .../Handler/SendPostIntentHandler.swift | 4 +- MastodonIntent/Model/Account+Fetch.swift | 37 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/MastodonIntent/Handler/SendPostIntentHandler.swift b/MastodonIntent/Handler/SendPostIntentHandler.swift index b457b1c91..0e37d63a8 100644 --- a/MastodonIntent/Handler/SendPostIntentHandler.swift +++ b/MastodonIntent/Handler/SendPostIntentHandler.swift @@ -59,7 +59,7 @@ extension SendPostIntentHandler: SendPostIntentHandling { } mastodonAuthentications = [authentication] } else { - mastodonAuthentications = try accounts.mastodonAuthentication(in: managedObjectContext) + mastodonAuthentications = try accounts.mastodonAuthentication() } let authenticationBoxes = mastodonAuthentications.map { authentication in @@ -149,7 +149,7 @@ extension SendPostIntentHandler: SendPostIntentHandling { } func provideAccountsOptionsCollection(for intent: SendPostIntent) async throws -> INObjectCollection { - let accounts = try await Account.fetch(in: managedObjectContext) + let accounts = try await Account.fetch() return .init(items: accounts) } diff --git a/MastodonIntent/Model/Account+Fetch.swift b/MastodonIntent/Model/Account+Fetch.swift index f3d8ee344..bdd9f412a 100644 --- a/MastodonIntent/Model/Account+Fetch.swift +++ b/MastodonIntent/Model/Account+Fetch.swift @@ -14,34 +14,29 @@ import MastodonCore extension Account { @MainActor - static func fetch(in managedObjectContext: NSManagedObjectContext) async throws -> [Account] { - // get accounts - let accounts: [Account] = try await managedObjectContext.perform { - let results = AuthenticationServiceProvider.shared.authentications - let accounts = results.compactMap { mastodonAuthentication -> Account? in - guard let user = mastodonAuthentication.user(in: managedObjectContext) else { - return nil - } - let account = Account( - identifier: mastodonAuthentication.identifier.uuidString, - display: user.displayNameWithFallback, - subtitle: user.acctWithDomain, - image: user.avatarImageURL().flatMap { INImage(url: $0) } - ) - account.name = user.displayNameWithFallback - account.username = user.acctWithDomain - return account + static func fetch() async throws -> [Account] { + let accounts = AuthenticationServiceProvider.shared.authentications.compactMap { mastodonAuthentication -> Account? in + guard let authenticatedAccount = mastodonAuthentication.account() else { + return nil } - return accounts - } // end managedObjectContext.perform + let account = Account( + identifier: mastodonAuthentication.identifier.uuidString, + display: authenticatedAccount.displayNameWithFallback, + subtitle: authenticatedAccount.acctWithDomain, + image: authenticatedAccount.avatarImageURL().flatMap { INImage(url: $0) } + ) + account.name = authenticatedAccount.displayNameWithFallback + account.username = authenticatedAccount.acctWithDomain + return account + } return accounts } - + } extension Array where Element == Account { - func mastodonAuthentication(in managedObjectContext: NSManagedObjectContext) throws -> [MastodonAuthentication] { + func mastodonAuthentication() throws -> [MastodonAuthentication] { let identifiers = self .compactMap { $0.identifier } .compactMap { UUID(uuidString: $0) } From 6b6ed281348bca5b8d48ee701ea6f3f9d1c96ee1 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Jan 2024 12:30:36 +0100 Subject: [PATCH 103/159] Emojis aren't optional (IOS-192) meet [] --- .../ForYou/ProfileCardView+Configuration.swift | 2 +- .../Share/View/Content/UserView+Configuration.swift | 2 +- .../SuggestionAccountTableViewCell.swift | 2 +- Mastodon/Scene/Thread/ThreadViewModel.swift | 2 +- .../Protocol/MastodonEmojiContainer.swift | 6 ++---- .../MastodonSDK/Entity/Mastodon+Entity+Account.swift | 4 ++-- .../MastodonSDK/Entity/Mastodon+Entity+Status.swift | 4 ++-- .../Entity/Mastodon+Entity+StatusEdit.swift | 2 +- .../MastodonUI/View/Content/CondensedUserView.swift | 2 +- .../View/Content/StatusView+Configuration.swift | 12 ++++++------ 10 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+Configuration.swift b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+Configuration.swift index 4192e59b7..ecb4e3db1 100644 --- a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+Configuration.swift +++ b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+Configuration.swift @@ -22,7 +22,7 @@ extension ProfileCardView { viewModel.followersCount = account.followersCount viewModel.authorAvatarImageURL = account.avatarImageURL() - let emojis = account.emojis?.asDictionary ?? [:] + let emojis = account.emojis.asDictionary do { let content = MastodonContent(content: account.displayNameWithFallback, emojis: emojis) diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index 4a3595c73..a2ccff02a 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -26,7 +26,7 @@ extension UserView { authorUsernameLabel.configure(content: authorUsername) do { - let emojis = account.emojis?.asDictionary ?? [:] + let emojis = account.emojis.asDictionary let content = MastodonContent(content: account.displayNameWithFallback, emojis: emojis) let metaContent = try MastodonMetaContent.convert(document: content) authorNameLabel.configure(content: metaContent) diff --git a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift index 17a8e5b55..c556ed1e5 100644 --- a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift +++ b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift @@ -91,7 +91,7 @@ final class SuggestionAccountTableViewCell: UITableViewCell { let metaContent: MetaContent = { do { - let mastodonContent = MastodonContent(content: account.note, emojis: account.emojis?.asDictionary ?? [:]) + let mastodonContent = MastodonContent(content: account.note, emojis: account.emojis.asDictionary) return try MastodonMetaContent.convert(document: mastodonContent) } catch { assertionFailure() diff --git a/Mastodon/Scene/Thread/ThreadViewModel.swift b/Mastodon/Scene/Thread/ThreadViewModel.swift index a6957049e..a164527a2 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -76,7 +76,7 @@ class ThreadViewModel { // bind titleView self.navigationBarTitle = { let title = L10n.Scene.Thread.title(status.entity.account.displayNameWithFallback) - let content = MastodonContent(content: title, emojis: status.entity.account.emojis?.asDictionary ?? [:]) + let content = MastodonContent(content: title, emojis: status.entity.account.emojis.asDictionary) return try? MastodonMetaContent.convert(document: content) }() } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonEmojiContainer.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonEmojiContainer.swift index 7273d507f..c017ac08e 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonEmojiContainer.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Protocol/MastodonEmojiContainer.swift @@ -11,14 +11,12 @@ import MastodonSDK import CoreDataStack public protocol MastodonEmojiContainer { - var emojis: [Mastodon.Entity.Emoji]? { get } + var emojis: [Mastodon.Entity.Emoji] { get } } extension MastodonEmojiContainer { public var mastodonEmojis: [MastodonEmoji] { - return emojis.flatMap { emojis in - emojis.map { MastodonEmoji(emoji: $0) } - } ?? [] + return emojis.map { MastodonEmoji(emoji: $0) } } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 836160db7..9e5a39019 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -35,7 +35,7 @@ extension Mastodon.Entity { public let header: String public let headerStatic: String? public let locked: Bool - public let emojis: [Emoji]? + public let emojis: [Emoji] public let discoverable: Bool? // Statistical @@ -52,7 +52,7 @@ extension Mastodon.Entity { public let suspended: Bool? public let muteExpiresAt: Date? - internal init(id: Mastodon.Entity.Account.ID, username: String, acct: String, url: String, displayName: String, note: String, avatar: String, avatarStatic: String? = nil, header: String, headerStatic: String? = nil, locked: Bool, emojis: [Mastodon.Entity.Emoji]? = nil, discoverable: Bool? = nil, createdAt: Date, lastStatusAt: Date? = nil, statusesCount: Int, followersCount: Int, followingCount: Int, moved: Mastodon.Entity.Account? = nil, fields: [Mastodon.Entity.Field]? = nil, bot: Bool? = nil, source: Mastodon.Entity.Source? = nil, suspended: Bool? = nil, muteExpiresAt: Date? = nil) { + internal init(id: Mastodon.Entity.Account.ID, username: String, acct: String, url: String, displayName: String, note: String, avatar: String, avatarStatic: String? = nil, header: String, headerStatic: String? = nil, locked: Bool, emojis: [Mastodon.Entity.Emoji] = [], discoverable: Bool? = nil, createdAt: Date, lastStatusAt: Date? = nil, statusesCount: Int, followersCount: Int, followingCount: Int, moved: Mastodon.Entity.Account? = nil, fields: [Mastodon.Entity.Field]? = nil, bot: Bool? = nil, source: Mastodon.Entity.Source? = nil, suspended: Bool? = nil, muteExpiresAt: Date? = nil) { self.id = id self.username = username self.acct = acct diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index bcfbf33f5..c39517ecf 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -37,8 +37,8 @@ extension Mastodon.Entity { // Rendering public let mentions: [Mention]? - public let tags: [Tag]? - public let emojis: [Emoji]? + public let tags: [Tag] + public let emojis: [Emoji] // Informational public let reblogsCount: Int diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+StatusEdit.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+StatusEdit.swift index fd61ff060..f8f50b7f5 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+StatusEdit.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+StatusEdit.swift @@ -36,7 +36,7 @@ extension Mastodon.Entity { public let account: Account public let poll: Poll? public let mediaAttachments: [Attachment]? - public let emojis: [Emoji]? + public let emojis: [Emoji] enum CodingKeys: String, CodingKey { case content diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift index c19d8f2ed..c549ad5a5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/CondensedUserView.swift @@ -127,7 +127,7 @@ public class CondensedUserView: UIView { public func configure(with account: Mastodon.Entity.Account, showFollowers: Bool = true) { let displayNameMetaContent: MetaContent do { - let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis?.asDictionary ?? [:]) + let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis.asDictionary) displayNameMetaContent = try MastodonMetaContent.convert(document: content) } catch { displayNameMetaContent = PlaintextMetaContent(string: account.displayNameWithFallback) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index b4238c80d..37cab065c 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -157,7 +157,7 @@ extension StatusView { statusID: inReplyToID, authorization: authenticationBox.userAuthorization ).singleOutput().value { - let header = createHeader(name: replyTo.account.displayNameWithFallback, emojis: replyTo.account.emojis?.asDictionary ?? [:]) + let header = createHeader(name: replyTo.account.displayNameWithFallback, emojis: replyTo.account.emojis.asDictionary) viewModel.header = header } } @@ -200,7 +200,7 @@ extension StatusView { // author avatar viewModel.authorAvatarImageURL = author.avatarImageURL() - let emojis = author.emojis?.asDictionary ?? [:] + let emojis = author.emojis.asDictionary // author name viewModel.authorName = { @@ -282,7 +282,7 @@ extension StatusView { // content do { - let content = MastodonContent(content: translatedContent, emojis: status.entity.emojis?.asDictionary ?? [:]) + let content = MastodonContent(content: translatedContent, emojis: status.entity.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent viewModel.isCurrentlyTranslating = false @@ -301,7 +301,7 @@ extension StatusView { viewModel.language = (status.reblog ?? status).entity.language // content do { - let content = MastodonContent(content: statusEdit.content, emojis: statusEdit.emojis?.asDictionary ?? [:]) + let content = MastodonContent(content: statusEdit.content, emojis: statusEdit.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent viewModel.isCurrentlyTranslating = false @@ -321,7 +321,7 @@ extension StatusView { // spoilerText if let spoilerText = status.entity.spoilerText, !spoilerText.isEmpty { do { - let content = MastodonContent(content: spoilerText, emojis: status.entity.emojis?.asDictionary ?? [:]) + let content = MastodonContent(content: spoilerText, emojis: status.entity.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.spoilerContent = metaContent } catch { @@ -335,7 +335,7 @@ extension StatusView { viewModel.language = (status.reblog ?? status).entity.language // content do { - let content = MastodonContent(content: status.entity.content ?? "", emojis: status.entity.emojis?.asDictionary ?? [:]) + let content = MastodonContent(content: status.entity.content ?? "", emojis: status.entity.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent viewModel.isCurrentlyTranslating = false From 22d0b45be79d9288e4805978559cd26c1d356ccc Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Jan 2024 12:56:08 +0100 Subject: [PATCH 104/159] Use account on account-switcher (IOS-192) --- Mastodon/Scene/Account/AccountListViewModel.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index 5919596bf..ea2206703 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -103,25 +103,25 @@ extension AccountListViewModel { authentication: MastodonAuthentication, activeAuthentication: MastodonAuthentication ) { - guard let user = authentication.user(in: context) else { return } - + guard let account = authentication.account() else { return } + // avatar cell.avatarButton.avatarImageView.configure( - configuration: .init(url: user.avatarImageURL()) + configuration: .init(url: account.avatarImageURL()) ) // name do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary) + let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) cell.nameLabel.configure(content: metaContent) } catch { assertionFailure() - cell.nameLabel.configure(content: PlaintextMetaContent(string: user.displayNameWithFallback)) + cell.nameLabel.configure(content: PlaintextMetaContent(string: account.displayNameWithFallback)) } // username - let usernameMetaContent = PlaintextMetaContent(string: "@" + user.acctWithDomain) + let usernameMetaContent = PlaintextMetaContent(string: "@" + account.acctWithDomain) cell.usernameLabel.configure(content: usernameMetaContent) // badge From 120b9e18b137dc82b8d636f0334a7436b1314786 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Jan 2024 12:59:59 +0100 Subject: [PATCH 105/159] Fix warnings --- .../Extension/MastodonSDK/Mastodon+Entity+Account.swift | 2 +- .../FeedFetchedResultsController.swift | 1 - .../Scene/ComposeContent/ComposeContentViewModel.swift | 2 +- .../FamiliarFollowersDashboardView+Configuration.swift | 2 +- .../MastodonUI/View/Content/StatusView+Configuration.swift | 4 ++-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift index 56c00e31d..0e4ba9966 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -14,7 +14,7 @@ extension Mastodon.Entity.Account { let isAnimated = !UserDefaults.shared.preferredStaticEmoji var dict = MastodonContent.Emojis() - for emoji in emojis ?? [] { + for emoji in emojis { dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL } return dict diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index ee2a1ac9b..c3b8886f5 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -10,7 +10,6 @@ import Foundation import UIKit import Combine import MastodonSDK -import MastodonCore final public class FeedFetchedResultsController { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index a70b35a42..b35d94f03 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -314,7 +314,7 @@ extension ComposeContentViewModel { self.avatarURL = account.avatarImageURL() do { - let content = MastodonContent(content: account.displayNameWithFallback, emojis: (account.emojis ?? []).asDictionary) + let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) self.name = metaContent } catch { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift index d8fbbf6ca..12aadb040 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/FamiliarFollowersDashboardView+Configuration.swift @@ -20,7 +20,7 @@ extension FamiliarFollowersDashboardView { viewModel.emojis = { var array: [Mastodon.Entity.Emoji] = [] for account in accounts { - array.append(contentsOf: account.emojis ?? []) + array.append(contentsOf: account.emojis) } return array.asDictionary }() diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index 37cab065c..998bae1f8 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -91,7 +91,7 @@ extension StatusView { let account = authenticationBox.authentication.account() { let name = account.displayNameWithFallback - let emojis = account.emojis ?? [] + let emojis = account.emojis viewModel.header = { let text = L10n.Common.Controls.Status.userReblogged(name) @@ -106,7 +106,7 @@ extension StatusView { }() } else if status.reblog != nil { let name = status.entity.account.displayNameWithFallback - let emojis = status.entity.account.emojis ?? [] + let emojis = status.entity.account.emojis viewModel.header = { let text = L10n.Common.Controls.Status.userReblogged(name) From 7ab194b15d75b32467314dcc04eb1bcdee2e871d Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Jan 2024 16:46:49 +0100 Subject: [PATCH 106/159] Remove remaining Tag-stuff (IOS-186) (#1205) --- .../Provider/DataSourceFacade+Hashtag.swift | 26 --- .../HashtagTimelineHeaderView.swift | 14 -- .../HashtagTimelineViewModel.swift | 13 +- .../CoreData 9.xcdatamodel/contents | 14 +- .../Entity/Mastodon/History.swift | 81 ------- .../Entity/Mastodon/MastodonUser.swift | 1 - .../CoreDataStack/Entity/Mastodon/Tag.swift | 215 ------------------ .../CoreDataStack/Tag+Property.swift | 45 ---- .../MastodonSDK/API/Mastodon+API.swift | 2 +- 9 files changed, 3 insertions(+), 408 deletions(-) delete mode 100644 MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/History.swift delete mode 100644 MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Tag.swift delete mode 100644 MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Tag+Property.swift diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift index 8c4844166..dcf21d99f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift @@ -28,30 +28,4 @@ extension DataSourceFacade { transition: .show ) } - - @MainActor - static func coordinateToHashtagScene( - provider: DataSourceProvider & AuthContextProvider, - tag: ManagedObjectRecord - ) async { - let managedObjectContext = provider.context.managedObjectContext - let _name: String? = try? await managedObjectContext.perform { - guard let tag = tag.object(in: managedObjectContext) else { return nil } - return tag.name - } - - guard let name = _name else { return } - - let hashtagTimelineViewModel = HashtagTimelineViewModel( - context: provider.context, - authContext: provider.authContext, - hashtag: name - ) - - _ = provider.coordinator.present( - scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), - from: provider, - transition: .show - ) - } } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineHeaderView.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineHeaderView.swift index 96bc4ed13..647546e0c 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineHeaderView.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineHeaderView.swift @@ -38,20 +38,6 @@ final class HashtagTimelineHeaderView: UIView { postsTodayCount: Int(entity.history?.first?.uses ?? "0") ?? 0 ) } - - static func from(_ entity: Tag) -> Self { - Data( - name: entity.name, - following: entity.following, - postCount: entity.histories.reduce(0) { res, acc in - res + (Int(acc.uses) ?? 0) - }, - participantsCount: entity.histories.reduce(0) { res, acc in - res + (Int(acc.accounts) ?? 0) - }, - postsTodayCount: Int(entity.histories.first?.uses ?? "0") ?? 0 - ) - } } let titleLabel = UILabel() diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index 84f8a62aa..9dcee7f9b 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -61,18 +61,7 @@ final class HashtagTimelineViewModel { } func viewWillAppear() { - let predicate = Tag.predicate( - domain: authContext.mastodonAuthenticationBox.domain, - name: hashtag - ) - - guard - let object = Tag.findOrFetch(in: context.managedObjectContext, matching: predicate) - else { - return hashtagDetails.send(hashtagDetails.value?.copy(following: false)) - } - - hashtagDetails.send(hashtagDetails.value?.copy(following: object.following)) + hashtagDetails.send(hashtagDetails.value?.copy(following: hashtagEntity.value?.following ?? false)) } } diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents index 062ae9d7b..60d6994d2 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -111,7 +111,6 @@ - @@ -249,15 +248,4 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/History.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/History.swift deleted file mode 100644 index 6fe703e84..000000000 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/History.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// History.swift -// CoreDataStack -// -// Created by sxiaojian on 2021/2/1. -// - -import CoreData -import Foundation - -public final class History: NSManagedObject { - public typealias ID = UUID - @NSManaged public private(set) var identifier: ID - @NSManaged public private(set) var createAt: Date - - @NSManaged public private(set) var day: Date - @NSManaged public private(set) var uses: String - @NSManaged public private(set) var accounts: String - - // many-to-one relationship - @NSManaged public private(set) var tag: Tag -} - -public extension History { - override func awakeFromInsert() { - super.awakeFromInsert() - setPrimitiveValue(UUID(), forKey: #keyPath(History.identifier)) - } - - @discardableResult - static func insert( - into context: NSManagedObjectContext, - property: Property - ) -> History { - let history: History = context.insertObject() - history.day = property.day - history.uses = property.uses - history.accounts = property.accounts - return history - } -} - -public extension History { - func update(day: Date) { - if self.day != day { - self.day = day - } - } - - func update(uses: String) { - if self.uses != uses { - self.uses = uses - } - } - - func update(accounts: String) { - if self.accounts != accounts { - self.accounts = accounts - } - } -} - -public extension History { - struct Property { - public let day: Date - public let uses: String - public let accounts: String - - public init(day: Date, uses: String, accounts: String) { - self.day = day - self.uses = uses - self.accounts = accounts - } - } -} - -extension History: Managed { - public static var defaultSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \History.createAt, ascending: false)] - } -} diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 7080ec3fd..0858f2a96 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -76,7 +76,6 @@ final public class MastodonUser: NSManagedObject { @NSManaged public private(set) var votePollOptions: Set @NSManaged public private(set) var votePolls: Set // relationships - @NSManaged public private(set) var followedTags: Set @NSManaged public private(set) var following: Set @NSManaged public private(set) var followingBy: Set @NSManaged public private(set) var followRequested: Set diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Tag.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Tag.swift deleted file mode 100644 index d95c9dcb3..000000000 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Tag.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// Tag.swift -// CoreDataStack -// -// Created by sxiaojian on 2021/2/1. -// - -import CoreData -import Foundation - -public final class Tag: NSManagedObject { - public typealias ID = UUID - - // sourcery: autoGenerateProperty - @NSManaged public private(set) var identifier: ID - // sourcery: autoGenerateProperty - @NSManaged public private(set) var domain: String - // sourcery: autoGenerateProperty - @NSManaged public private(set) var createAt: Date - // sourcery: autoUpdatableObject, autoGenerateProperty - @NSManaged public private(set) var updatedAt: Date - - // sourcery: autoGenerateProperty - @NSManaged public private(set) var name: String - // sourcery: autoUpdatableObject, autoGenerateProperty - @NSManaged public private(set) var url: String - // sourcery: autoUpdatableObject, autoGenerateProperty - @NSManaged public private(set) var following: Bool - - // one-to-one relationship - - // many-to-many relationship - @NSManaged public private(set) var followedBy: Set -} - -extension Tag { - // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var histories: [MastodonTagHistory] { - get { - let keyPath = #keyPath(Tag.histories) - willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data - didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let attachments = try JSONDecoder().decode([MastodonTagHistory].self, from: data) - return attachments - } catch { - assertionFailure(error.localizedDescription) - return [] - } - } - set { - let keyPath = #keyPath(Tag.histories) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) - } - } -} - -extension Tag { - @discardableResult - public static func insert( - into context: NSManagedObjectContext, - property: Property - ) -> Tag { - let object: Tag = context.insertObject() - - object.configure(property: property) - - return object - } -} - - -extension Tag: Managed { - public static var defaultSortDescriptors: [NSSortDescriptor] { - [NSSortDescriptor(keyPath: \Tag.createAt, ascending: false)] - } -} - -public extension Tag { - - static func predicate(domain: String) -> NSPredicate { - NSPredicate(format: "%K == %@", #keyPath(Tag.domain), domain) - } - - static func predicate(name: String) -> NSPredicate { - // use case-insensitive query as tags #CaN #BE #speLLed #USiNG #arbITRARy #cASe - NSPredicate(format: "%K MATCHES[c] %@", #keyPath(Tag.name), name) - } - - static func predicate(domain: String, following: Bool) -> NSPredicate { - NSPredicate(format: "%K == %@ AND %K == %d", #keyPath(Tag.domain), domain, #keyPath(Tag.following), following) - } - - static func predicate(followedBy user: MastodonUser) -> NSPredicate { - NSPredicate(format: "ANY %K.%K == %@", #keyPath(Tag.followedBy), #keyPath(MastodonUser.id), user.id) - } - - static func predicate(domain: String, name: String) -> NSPredicate { - NSCompoundPredicate(andPredicateWithSubpredicates: [ - predicate(domain: domain), - predicate(name: name), - ]) - } - - static func predicate(domain: String, following: Bool, by user: MastodonUser) -> NSPredicate { - NSCompoundPredicate(andPredicateWithSubpredicates: [ - predicate(domain: domain, following: following), - predicate(followedBy: user) - ]) - } -} - -// MARK: - AutoGenerateProperty -extension Tag: AutoGenerateProperty { - // sourcery:inline:Tag.AutoGenerateProperty - - // Generated using Sourcery - // DO NOT EDIT - public struct Property { - public let identifier: ID - public let domain: String - public let createAt: Date - public let updatedAt: Date - public let name: String - public let url: String - public let following: Bool - public let histories: [MastodonTagHistory] - - public init( - identifier: ID, - domain: String, - createAt: Date, - updatedAt: Date, - name: String, - url: String, - following: Bool, - histories: [MastodonTagHistory] - ) { - self.identifier = identifier - self.domain = domain - self.createAt = createAt - self.updatedAt = updatedAt - self.name = name - self.url = url - self.following = following - self.histories = histories - } - } - - public func configure(property: Property) { - self.identifier = property.identifier - self.domain = property.domain - self.createAt = property.createAt - self.updatedAt = property.updatedAt - self.name = property.name - self.url = property.url - self.following = property.following - self.histories = property.histories - } - - public func update(property: Property) { - update(updatedAt: property.updatedAt) - update(url: property.url) - update(following: property.following) - update(histories: property.histories) - } - // sourcery:end -} - -// MARK: - AutoUpdatableObject -extension Tag: AutoUpdatableObject { - // sourcery:inline:Tag.AutoUpdatableObject - - // Generated using Sourcery - // DO NOT EDIT - public func update(updatedAt: Date) { - if self.updatedAt != updatedAt { - self.updatedAt = updatedAt - } - } - public func update(url: String) { - if self.url != url { - self.url = url - } - } - public func update(following: Bool) { - if self.following != following { - self.following = following - } - } - public func update(histories: [MastodonTagHistory]) { - if self.histories != histories { - self.histories = histories - } - } - // sourcery:end - - public func update(followed: Bool, by mastodonUser: MastodonUser) { - if following { - if !self.followedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Tag.followedBy)).add(mastodonUser) - } - } else { - if self.followedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Tag.followedBy)).remove(mastodonUser) - } - } - } - -} diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Tag+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Tag+Property.swift deleted file mode 100644 index 7411fd960..000000000 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Tag+Property.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Tag+Property.swift -// Mastodon -// -// Created by MainasuK on 2022-1-20. -// - -import Foundation -import CoreDataStack -import MastodonSDK - -extension Tag.Property { - public init( - entity: Mastodon.Entity.Tag, - domain: String, - networkDate: Date - ) { - self.init( - identifier: UUID(), - domain: domain, - createAt: networkDate, - updatedAt: networkDate, - name: entity.name, - url: entity.url, - following: entity.following ?? false, - histories: { - guard let histories = entity.history else { return [] } - let result: [MastodonTagHistory] = histories.map { history in - return MastodonTagHistory(entity: history) - } - return result - }() - ) - } -} - -extension MastodonTagHistory { - public convenience init(entity: Mastodon.Entity.History) { - self.init( - day: entity.day, - uses: entity.uses, - accounts: entity.accounts - ) - } -} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 4175e8444..8005ebfa1 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -211,7 +211,7 @@ extension Mastodon.API { return try Mastodon.API.decoder.decode(type, from: data) } catch let decodeError { #if DEBUG - debugPrint(decodeError) + debugPrint("\(response.url), Data: \(String(data: data, encoding: .utf8)), \(decodeError)") #endif guard let httpURLResponse = response as? HTTPURLResponse else { From 0b959f5bcae332afb8f57929a7e11e3e6dd03b83 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 5 Jan 2024 16:50:44 +0100 Subject: [PATCH 107/159] Remove private-note and notification from user (IOS-192) --- .../Provider/DataSourceFacade+Follow.swift | 1 - .../Provider/DataSourceProvider.swift | 1 - .../NotificationView+Configuration.swift | 1 - .../CoreData 9.xcdatamodel/contents | 24 -- .../CoreDataStack/Entity/App/Feed.swift | 13 - .../Entity/Mastodon/MastodonUser.swift | 1 - .../Entity/Mastodon/Notification.swift | 269 ------------------ .../Entity/Mastodon/PrivateNote.swift | 56 ---- .../CoreDataStack/Notification+Property.swift | 29 -- .../Persistence+Notification.swift | 197 ------------- .../Service/API/APIService+Block.swift | 18 -- .../Service/API/APIService+Mute.swift | 15 - .../Service/API/APIService+Notification.swift | 83 ------ 13 files changed, 708 deletions(-) delete mode 100644 MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift delete mode 100644 MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/PrivateNote.swift delete mode 100644 MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift delete mode 100644 MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 134e00ccb..9037fde33 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -7,7 +7,6 @@ import UIKit import CoreDataStack -import class CoreDataStack.Notification import MastodonCore import MastodonSDK import MastodonLocalization diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index 6334870b8..e8e58dd17 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -9,7 +9,6 @@ import UIKit import CoreDataStack import MastodonSDK -import class CoreDataStack.Notification enum DataSourceItem: Hashable { case status(record: MastodonStatus) diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index 98dec5397..f03fc3fb7 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -15,7 +15,6 @@ import Meta import MastodonAsset import MastodonCore import MastodonLocalization -import class CoreDataStack.Notification import MastodonSDK extension NotificationView { diff --git a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents index 60d6994d2..9e64f9623 100644 --- a/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents +++ b/MastodonSDK/Sources/CoreDataStack/CoreData.xcdatamodeld/CoreData 9.xcdatamodel/contents @@ -53,7 +53,6 @@ - @@ -119,10 +118,7 @@ - - - @@ -130,19 +126,6 @@ - - - - - - - - - - - - - @@ -168,12 +151,6 @@ - - - - - - @@ -216,7 +193,6 @@ - diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/App/Feed.swift b/MastodonSDK/Sources/CoreDataStack/Entity/App/Feed.swift index 5fca61153..2565a5e41 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/App/Feed.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/App/Feed.swift @@ -96,19 +96,6 @@ extension Feed { public static func hasNotificationPredicate() -> NSPredicate { return NSPredicate(format: "%K != nil", #keyPath(Feed.notification)) } - - public static func notificationTypePredicate(types: [MastodonNotificationType]) -> NSPredicate { - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - hasNotificationPredicate(), - NSPredicate( - format: "%K.%K IN %@", - #keyPath(Feed.notification), - #keyPath(Notification.typeRaw), - types.map { $0.rawValue } - ) - ]) - } - } // MARK: - AutoGenerateProperty diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 0858f2a96..807515ec8 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -66,7 +66,6 @@ final public class MastodonUser: NSManagedObject { // one-to-many relationship @NSManaged public private(set) var statuses: Set - @NSManaged public private(set) var notifications: Set // many-to-many relationship @NSManaged public private(set) var favourite: Set diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift deleted file mode 100644 index 7eec86842..000000000 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// Notification.swift -// CoreDataStack -// -// Created by sxiaojian on 2021/4/13. -// - -import Foundation -import CoreData - -public final class Notification: NSManagedObject { - public typealias ID = String - - // sourcery: autoGenerateProperty - @NSManaged public private(set) var id: ID - // sourcery: autoGenerateProperty - @NSManaged public private(set) var typeRaw: String - // sourcery: autoGenerateProperty - @NSManaged public private(set) var domain: String - // sourcery: autoGenerateProperty - @NSManaged public private(set) var userID: String - - // sourcery: autoGenerateProperty - @NSManaged public private(set) var createAt: Date - // sourcery: autoUpdatableObject, autoGenerateProperty - @NSManaged public private(set) var updatedAt: Date - - // one-to-one relationship - // sourcery: autoGenerateRelationship - @NSManaged public private(set) var account: MastodonUser - // sourcery: autoGenerateRelationship - @NSManaged public private(set) var status: Status? - - // many-to-one relationship - @NSManaged public private(set) var feeds: Set - -} - -extension Notification { - // sourcery: autoUpdatableObject - @objc public var followRequestState: MastodonFollowRequestState { - get { - let keyPath = #keyPath(Notification.followRequestState) - willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data - didAccessValue(forKey: keyPath) - do { - guard let data = _data, !data.isEmpty else { return .init(state: .none) } - let state = try JSONDecoder().decode(MastodonFollowRequestState.self, from: data) - return state - } catch { - assertionFailure(error.localizedDescription) - return .init(state: .none) - } - } - set { - let keyPath = #keyPath(Notification.followRequestState) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) - } - } - - // sourcery: autoUpdatableObject - @objc public var transientFollowRequestState: MastodonFollowRequestState { - get { - let keyPath = #keyPath(Notification.transientFollowRequestState) - willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data - didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return .init(state: .none) } - let state = try JSONDecoder().decode(MastodonFollowRequestState.self, from: data) - return state - } catch { - assertionFailure(error.localizedDescription) - return .init(state: .none) - } - } - set { - let keyPath = #keyPath(Notification.transientFollowRequestState) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) - } - } -} - -extension Notification: FeedIndexable { } - -extension Notification { - @discardableResult - public static func insert( - into context: NSManagedObjectContext, - property: Property, - relationship: Relationship - ) -> Notification { - let object: Notification = context.insertObject() - - object.configure(property: property) - object.configure(relationship: relationship) - - return object - } -} - -extension Notification: Managed { - public static var defaultSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \Notification.createAt, ascending: false)] - } -} - -extension Notification { - static func predicate(domain: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Notification.domain), domain) - } - - static func predicate(userID: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Notification.userID), userID) - } - - static func predicate(id: ID) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Notification.id), id) - } - - static func predicate(typeRaw: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Notification.typeRaw), typeRaw) - } - - public static func predicate( - domain: String, - userID: String, - id: ID - ) -> NSPredicate { - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - Notification.predicate(domain: domain), - Notification.predicate(userID: userID), - Notification.predicate(id: id) - ]) - } - - public static func predicate( - domain: String, - userID: String, - typeRaw: String? = nil - ) -> NSPredicate { - if let typeRaw = typeRaw { - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - Notification.predicate(domain: domain), - Notification.predicate(typeRaw: typeRaw), - Notification.predicate(userID: userID), - ]) - } else { - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - Notification.predicate(domain: domain), - Notification.predicate(userID: userID) - ]) - } - } - - public static func predicate(validTypesRaws types: [String]) -> NSPredicate { - return NSPredicate(format: "%K IN %@", #keyPath(Notification.typeRaw), types) - } - -} - -// MARK: - AutoGenerateProperty -extension Notification: AutoGenerateProperty { - // sourcery:inline:Notification.AutoGenerateProperty - - // Generated using Sourcery - // DO NOT EDIT - public struct Property { - public let id: ID - public let typeRaw: String - public let domain: String - public let userID: String - public let createAt: Date - public let updatedAt: Date - - public init( - id: ID, - typeRaw: String, - domain: String, - userID: String, - createAt: Date, - updatedAt: Date - ) { - self.id = id - self.typeRaw = typeRaw - self.domain = domain - self.userID = userID - self.createAt = createAt - self.updatedAt = updatedAt - } - } - - public func configure(property: Property) { - self.id = property.id - self.typeRaw = property.typeRaw - self.domain = property.domain - self.userID = property.userID - self.createAt = property.createAt - self.updatedAt = property.updatedAt - } - - public func update(property: Property) { - update(updatedAt: property.updatedAt) - } - // sourcery:end -} - -// MARK: - AutoGenerateRelationship -extension Notification: AutoGenerateRelationship { - // sourcery:inline:Notification.AutoGenerateRelationship - - // Generated using Sourcery - // DO NOT EDIT - public struct Relationship { - public let account: MastodonUser - public let status: Status? - - public init( - account: MastodonUser, - status: Status? - ) { - self.account = account - self.status = status - } - } - - public func configure(relationship: Relationship) { - self.account = relationship.account - self.status = relationship.status - } - // sourcery:end -} - -// MARK: - AutoUpdatableObject -extension Notification: AutoUpdatableObject { - // sourcery:inline:Notification.AutoUpdatableObject - - // Generated using Sourcery - // DO NOT EDIT - public func update(updatedAt: Date) { - if self.updatedAt != updatedAt { - self.updatedAt = updatedAt - } - } - public func update(followRequestState: MastodonFollowRequestState) { - if self.followRequestState != followRequestState { - self.followRequestState = followRequestState - } - } - public func update(transientFollowRequestState: MastodonFollowRequestState) { - if self.transientFollowRequestState != transientFollowRequestState { - self.transientFollowRequestState = transientFollowRequestState - } - } - // sourcery:end -} - -extension Notification { - public func attach(feed: Feed) { - mutableSetValue(forKey: #keyPath(Notification.feeds)).add(feed) - } -} diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/PrivateNote.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/PrivateNote.swift deleted file mode 100644 index 2e02db25c..000000000 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/PrivateNote.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// PrivateNote.swift -// CoreDataStack -// -// Created by MainasuK Cirno on 2021-4-1. -// - -import CoreData -import Foundation - -final public class PrivateNote: NSManagedObject { - - @NSManaged public private(set) var note: String? - - @NSManaged public private(set) var updatedAt: Date - - // many-to-one relationship - @NSManaged public private(set) var to: MastodonUser? - @NSManaged public private(set) var from: MastodonUser - -} - -extension PrivateNote { - public override func awakeFromInsert() { - super.awakeFromInsert() - setPrimitiveValue(Date(), forKey: #keyPath(PrivateNote.updatedAt)) - } - - @discardableResult - public static func insert( - into context: NSManagedObjectContext, - property: Property - ) -> PrivateNote { - let privateNode: PrivateNote = context.insertObject() - privateNode.note = property.note - return privateNode - } -} - -extension PrivateNote { - public struct Property { - public let note: String? - - init(note: String) { - self.note = note - } - } - -} - -extension PrivateNote: Managed { - public static var defaultSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \PrivateNote.updatedAt, ascending: false)] - } -} - diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift deleted file mode 100644 index 30d95a810..000000000 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Notification+Property.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Notification+Property.swift -// Mastodon -// -// Created by MainasuK on 2022-1-21. -// - -import Foundation -import CoreDataStack -import MastodonSDK -import class CoreDataStack.Notification - -extension Notification.Property { - public init( - entity: Mastodon.Entity.Notification, - domain: String, - userID: String, - networkDate: Date - ) { - self.init( - id: entity.id, - typeRaw: entity.type.rawValue, - domain: domain, - userID: userID, - createAt: entity.createdAt, - updatedAt: networkDate - ) - } -} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift deleted file mode 100644 index cfe715503..000000000 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// Persistence+Notification.swift -// Mastodon -// -// Created by MainasuK on 2022-1-21. -// - -import CoreData -import CoreDataStack -import Foundation -import MastodonSDK -import class CoreDataStack.Notification - -extension Persistence.Notification { - - public struct PersistContext { - public let domain: String - public let entity: Mastodon.Entity.Notification - public let me: MastodonUser - public let networkDate: Date - - public init( - domain: String, - entity: Mastodon.Entity.Notification, - me: MastodonUser, - networkDate: Date - ) { - self.domain = domain - self.entity = entity - self.me = me - self.networkDate = networkDate - } - } - - public struct PersistResult { - public let notification: Notification - public let isNewInsertion: Bool - - public init( - notification: Notification, - isNewInsertion: Bool - ) { - self.notification = notification - self.isNewInsertion = isNewInsertion - } - } - - public static func createOrMerge( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> PersistResult { - - if let old = fetch(in: managedObjectContext, context: context) { - merge(object: old, context: context) - return PersistResult( - notification: old, - isNewInsertion: false - ) - } else { - let accountResult = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: context.domain, - entity: context.entity.account, - cache: nil, - networkDate: context.networkDate - ) - ) - let account = accountResult.user - - let status: Status? = { - guard let entity = context.entity.status else { return nil } - let result = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: context.domain, - entity: entity, - me: context.me, - statusCache: nil, - userCache: nil, - networkDate: context.networkDate - ) - ) - return result.status - }() - - let relationship = Notification.Relationship( - account: account, - status: status - ) - - let object = create( - in: managedObjectContext, - context: context, - relationship: relationship - ) - - return PersistResult( - notification: object, - isNewInsertion: true - ) - } - } - -} - -extension Persistence.Notification { - - public static func fetch( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> Notification? { - let request = Notification.sortedFetchRequest - request.predicate = Notification.predicate( - domain: context.me.domain, - userID: context.me.id, - id: context.entity.id - ) - request.fetchLimit = 1 - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - } - - @discardableResult - public static func create( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext, - relationship: Notification.Relationship - ) -> Notification { - let property = Notification.Property( - entity: context.entity, - domain: context.me.domain, - userID: context.me.id, - networkDate: context.networkDate - ) - let object = Notification.insert( - into: managedObjectContext, - property: property, - relationship: relationship - ) - update(object: object, context: context) - return object - } - - public static func merge( - object: Notification, - context: PersistContext - ) { - guard context.networkDate > object.updatedAt else { return } - let property = Notification.Property( - entity: context.entity, - domain: context.me.domain, - userID: context.me.id, - networkDate: context.networkDate - ) - object.update(property: property) - - if let status = object.status, let entity = context.entity.status { - let property = Status.Property( - entity: entity, - domain: context.domain, - networkDate: context.networkDate - ) - status.update(property: property) - } - - let accountProperty = MastodonUser.Property( - entity: context.entity.account, - domain: context.domain, - networkDate: context.networkDate - ) - object.account.update(property: accountProperty) - - if let author = object.status, let entity = context.entity.status { - let property = Status.Property( - entity: entity, - domain: context.domain, - networkDate: context.networkDate - ) - author.update(property: property) - } - - update(object: object, context: context) - } - - private static func update( - object: Notification, - context: PersistContext - ) { - // do nothing - } - -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index c24a3b703..524e01db6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -68,21 +68,3 @@ extension APIService { return response } } - -extension MastodonUser { - func deleteStatusAndNotificationFeeds(in context: NSManagedObjectContext) { - statuses.map { - $0.feeds - .union($0.reblogFrom.map { $0.feeds }.flatMap { $0 }) - .union($0.notifications.map { $0.feeds }.flatMap { $0 }) - } - .flatMap { $0 } - .forEach(context.delete) - - notifications.map { - $0.feeds - } - .flatMap { $0 } - .forEach(context.delete) - } -} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index 5100da887..cbf9d5ed3 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -40,21 +40,6 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - let userIDs = response.value.map { $0.id } - let predicate = MastodonUser.predicate(domain: authenticationBox.domain, ids: userIDs) - - let fetchRequest = MastodonUser.fetchRequest() - fetchRequest.predicate = predicate - fetchRequest.includesPropertyValues = false - - try await managedObjectContext.performChanges { - let users = try managedObjectContext.fetch(fetchRequest) as! [MastodonUser] - - for user in users { - user.deleteStatusAndNotificationFeeds(in: managedObjectContext) - } - } - return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 296a43d2b..e7889f4aa 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -11,7 +11,6 @@ import CoreDataStack import Foundation import MastodonSDK import OSLog -import class CoreDataStack.Notification extension APIService { @@ -86,74 +85,6 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { - assertionFailure() - return - } - - var notifications: [Notification] = [] - for entity in response.value { - let result = Persistence.Notification.createOrMerge( - in: managedObjectContext, - context: Persistence.Notification.PersistContext( - domain: authenticationBox.domain, - entity: entity, - me: me, - networkDate: response.networkDate - ) - ) - notifications.append(result.notification) - } - - // locate anchor notification - let anchorNotification: Notification? = { - guard let maxID = query.maxID else { return nil } - let request = Notification.sortedFetchRequest - request.predicate = Notification.predicate( - domain: authenticationBox.domain, - userID: authenticationBox.userID, - id: maxID - ) - request.fetchLimit = 1 - return try? managedObjectContext.fetch(request).first - }() - - // update hasMore flag for anchor status - let acct = Feed.Acct.mastodon(domain: authenticationBox.domain, userID: authenticationBox.userID) - let kind: Feed.Kind = scope == .everything ? .notificationAll : .notificationMentions - if let anchorNotification = anchorNotification, - let feed = anchorNotification.feed(kind: kind, acct: acct) { - feed.update(hasMore: false) - } - - // persist Feed relationship - let sortedNotifications = notifications.sorted(by: { $0.createAt < $1.createAt }) - let oldestNotification = sortedNotifications.first - for notification in notifications { - let _feed = notification.feed(kind: kind, acct: acct) - if let feed = _feed { - feed.update(updatedAt: response.networkDate) - } else { - let feedProperty = Feed.Property( - acct: acct, - kind: kind, - hasMore: false, - createdAt: notification.createAt, - updatedAt: response.networkDate - ) - let feed = Feed.insert(into: managedObjectContext, property: feedProperty) - notification.attach(feed: feed) - - // set hasMore on oldest notification if is new feed - if notification === oldestNotification { - feed.update(hasMore: true) - } - } - } - } - return response } } @@ -174,20 +105,6 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - _ = Persistence.Notification.createOrMerge( - in: managedObjectContext, - context: Persistence.Notification.PersistContext( - domain: domain, - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - } - return response } From fe5c88254b48421c853a7685776b0fcf93435ff0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 8 Jan 2024 22:48:35 +0100 Subject: [PATCH 108/159] Fix warnings I guess that they made their way back into the app due to the merge --- Mastodon/Protocol/Provider/DataSourceFacade+Media.swift | 1 - .../DataSourceProvider+NotificationTableViewCellDelegate.swift | 1 - .../NotificationTimelineViewController.swift | 1 - .../NotificationTimelineViewModel+LoadOldestState.swift | 1 - .../SearchResultOverviewCoordinator.swift | 1 - Mastodon/Scene/Thread/RemoteThreadViewModel.swift | 2 -- Mastodon/Supporting Files/SceneDelegate.swift | 2 -- .../Sources/MastodonCore/Service/API/APIService+Block.swift | 1 - .../Sources/MastodonCore/Service/API/APIService+Mute.swift | 1 - 9 files changed, 11 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift index 9608d8257..1cbc15b9d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift @@ -65,7 +65,6 @@ extension DataSourceFacade { status: MastodonStatus, previewContext: AttachmentPreviewContext ) async throws { - let managedObjectContext = dependency.context.managedObjectContext let status = status.reblog ?? status let attachments = status.entity.mastodonAttachments diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 7f139428f..5f09efc24 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -220,7 +220,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med return } - let managedObjectContext = self.context.managedObjectContext let _mediaTransitionContext: NotificationMediaTransitionContext? = { guard let status = record.status?.reblog ?? record.status else { return nil } return NotificationMediaTransitionContext( diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 05d290742..85a810094 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -276,7 +276,6 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow else { return } guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPathForSelectedRow) else { return } - let domain = authContext.mastodonAuthenticationBox.domain Task { @MainActor in switch item { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index 17222acd7..099961856 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -54,7 +54,6 @@ extension NotificationTimelineViewModel.LoadOldestState { let scope = viewModel.scope Task { - let managedObjectContext = viewModel.context.managedObjectContext let _maxID: Mastodon.Entity.Notification.ID? = lastFeedRecord.notification?.id guard let maxID = _maxID else { diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift index 71b0e599f..7e78e3b61 100644 --- a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift +++ b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift @@ -69,7 +69,6 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl ) let authContext = self.authContext - let managedObjectContext = context.managedObjectContext Task { let searchResult = try await context.apiService.search( diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift index 1585b524b..8c2737111 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -24,7 +24,6 @@ final class RemoteThreadViewModel: ThreadViewModel { ) Task { @MainActor in - let domain = authContext.mastodonAuthenticationBox.domain let response = try await context.apiService.status( statusID: statusID, authenticationBox: authContext.mastodonAuthenticationBox @@ -48,7 +47,6 @@ final class RemoteThreadViewModel: ThreadViewModel { ) Task { @MainActor in - let domain = authContext.mastodonAuthenticationBox.domain let response = try await context.apiService.notification( notificationID: notificationID, authenticationBox: authContext.mastodonAuthenticationBox diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 6cfb3b482..42c23b433 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -138,7 +138,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { switch (profile, statusID) { case (profile, nil): Task { - let domain = authContext.mastodonAuthenticationBox.domain let authenticationBox = authContext.mastodonAuthenticationBox guard let me = authenticationBox.authentication.account() else { return } @@ -285,7 +284,6 @@ extension SceneDelegate { Task { do { - let domain = authContext.mastodonAuthenticationBox.domain let authenticationBox = authContext.mastodonAuthenticationBox guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift index 524e01db6..9b0a1a465 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Block.swift @@ -27,7 +27,6 @@ extension APIService { limit: Int? = nil, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { - let managedObjectContext = backgroundManagedObjectContext let response = try await Mastodon.API.Account.blocks( session: session, domain: authenticationBox.domain, diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index cbf9d5ed3..f973412ab 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -31,7 +31,6 @@ extension APIService { limit: Int?, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { - let managedObjectContext = backgroundManagedObjectContext let response = try await Mastodon.API.Account.mutes( session: session, domain: authenticationBox.domain, From 923ff39b04a480451287feeaa9bf84ba05447774 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Jan 2024 17:02:55 +0100 Subject: [PATCH 109/159] Fix indention Co-authored-by: Marcus Kida --- Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index dcdb3e93c..840014f6f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -61,6 +61,12 @@ extension DataSourceFacade { domain: domain, authenticationBox: provider.authContext.mastodonAuthenticationBox) else { return provider.coordinator.hideLoading() + guard let account = try await provider.context.apiService.fetchUser( + username: username, + domain: domain, + authenticationBox: provider.authContext.mastodonAuthenticationBox + ) else { + return provider.coordinator.hideLoading() } provider.coordinator.hideLoading() From 97d1ab4e5b839e7ebe8a38c0b4b09f8a1bff1552 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Jan 2024 17:44:06 +0100 Subject: [PATCH 110/159] Fix build (IOS-192) --- Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 840014f6f..78c01c151 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -57,10 +57,6 @@ extension DataSourceFacade { Task { do { - guard let account = try await provider.context.apiService.fetchUser(username: username, - domain: domain, - authenticationBox: provider.authContext.mastodonAuthenticationBox) else { - return provider.coordinator.hideLoading() guard let account = try await provider.context.apiService.fetchUser( username: username, domain: domain, From e70a67caf0168b92eb88c7741c14aca0c057d9da Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Jan 2024 17:45:01 +0100 Subject: [PATCH 111/159] Move task and async/await to call-site (IOS-192) --- .../Provider/DataSourceFacade+Profile.swift | 37 +++++++++---------- ...taSourceProvider+UITableViewDelegate.swift | 2 +- .../DiscoveryForYouViewController.swift | 4 +- .../NotificationTimelineViewController.swift | 2 +- .../SearchHistoryViewController.swift | 2 +- ...ultViewController+DataSourceProvider.swift | 2 +- .../Scene/Settings/SettingsCoordinator.swift | 7 +++- .../SuggestionAccountViewController.swift | 6 ++- 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 78c01c151..d0e38986b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -41,6 +41,7 @@ extension DataSourceFacade { assertionFailure() return } + await coordinateToProfileScene( provider: provider, account: redirectRecord @@ -100,30 +101,26 @@ extension DataSourceFacade { } @MainActor - static func coordinateToProfileScene( + public static func coordinateToProfileScene( provider: ViewControllerWithDependencies & AuthContextProvider, account: Mastodon.Entity.Account - ) { + ) async { + guard let me = provider.authContext.mastodonAuthenticationBox.authentication.account(), + let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else { return } - Task { @MainActor in + let profileViewModel = ProfileViewModel( + context: provider.context, + authContext: provider.authContext, + account: account, + relationship: relationship, + me: me + ) - guard let me = provider.authContext.mastodonAuthenticationBox.authentication.account(), - let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else { return } - - let profileViewModel = ProfileViewModel( - context: provider.context, - authContext: provider.authContext, - account: account, - relationship: relationship, - me: me - ) - - _ = provider.coordinator.present( - scene: .profile(viewModel: profileViewModel), - from: provider, - transition: .show - ) - } + _ = provider.coordinator.present( + scene: .profile(viewModel: profileViewModel), + from: provider, + transition: .show + ) } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 6f092b9e9..dc2efdeeb 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -23,7 +23,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid } switch item { case .account(let account, relationship: _): - DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) case .status(let status): await DataSourceFacade.coordinateToStatusThreadScene( provider: self, diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift index 1de2dd704..290f12202 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift @@ -99,7 +99,9 @@ extension DiscoveryForYouViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard case let .account(account, _) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } - DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + Task { + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + } } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 85a810094..8c88afde2 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -295,7 +295,7 @@ extension NotificationTimelineViewController: TableViewControllerNavigateable { ) } else { - DataSourceFacade.coordinateToProfileScene(provider: self, account: notification.account) + await DataSourceFacade.coordinateToProfileScene(provider: self, account: notification.account) } default: break diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift index dd98bf463..a01100be7 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController.swift @@ -75,7 +75,7 @@ extension SearchHistoryViewController: UICollectionViewDelegate { switch item { case .account(account: let account, relationship: _): - DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) case .hashtag(let tag): await DataSourceFacade.coordinateToHashtagScene( diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index 81d20edb2..638ad9a15 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -62,7 +62,7 @@ extension SearchResultViewController { switch item { case .account(let account, relationship: _): - DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) case .status(let status): await DataSourceFacade.coordinateToStatusThreadScene( provider: self, diff --git a/Mastodon/Scene/Settings/SettingsCoordinator.swift b/Mastodon/Scene/Settings/SettingsCoordinator.swift index 8d9e40695..4be5f27d9 100644 --- a/Mastodon/Scene/Settings/SettingsCoordinator.swift +++ b/Mastodon/Scene/Settings/SettingsCoordinator.swift @@ -216,8 +216,11 @@ extension SettingsCoordinator: ServerDetailsViewControllerDelegate { } extension SettingsCoordinator: AboutInstanceViewControllerDelegate { - @MainActor func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) { - DataSourceFacade.coordinateToProfileScene(provider: viewController, account: account) + @MainActor + func showAdminAccount(_ viewController: AboutInstanceViewController, account: Mastodon.Entity.Account) { + Task { + await DataSourceFacade.coordinateToProfileScene(provider: viewController, account: account) + } } func sendEmailToAdmin(_ viewController: AboutInstanceViewController, emailAddress: String) { diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 3962feff7..ad39d4e8a 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -90,8 +90,10 @@ extension SuggestionAccountViewController: UITableViewDelegate { guard let tableViewDiffableDataSource = viewModel.tableViewDiffableDataSource else { return } guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { - case .account(let account, _): - DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + case .account(let account, _): + Task { + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + } } tableView.deselectRow(at: indexPath, animated: true) From cfa0e9a2bc790c0461daff4706a30fc7bd7c6e6f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 9 Jan 2024 17:58:48 +0100 Subject: [PATCH 112/159] Fix indention, fix warnings (IOS-192) --- ...er+NotificationTableViewCellDelegate.swift | 24 ++++----- ...Provider+StatusTableViewCellDelegate.swift | 50 ++++++------------- 2 files changed, 27 insertions(+), 47 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 5f09efc24..700105814 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -480,18 +480,18 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } switch item { - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .account(let account, let relationship): - await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) - case .notification: - assertionFailure("TODO") - case .hashtag(_): - assertionFailure("TODO") + case .status(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + case .account(let account, _): + await DataSourceFacade.coordinateToProfileScene(provider: self, account: account) + case .notification: + assertionFailure("TODO") + case .hashtag(_): + assertionFailure("TODO") } } // end Task } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 9d75caf6d..e06848f4b 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -122,16 +122,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte didTapCardWithURL url: URL ) { Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .status(status) = item else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.responseToURLAction( provider: self, url: url @@ -146,16 +136,6 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte didTapURL url: URL ) { Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .status(status) = item else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.responseToURLAction( provider: self, url: url @@ -682,21 +662,21 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte return } switch item { - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .status, // remove reblog wrapper - status: status - ) - case .account(let account, _): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - account: account - ) - case .notification: - assertionFailure("TODO") - case .hashtag(_): - assertionFailure("TODO") + case .status(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + target: .status, // remove reblog wrapper + status: status + ) + case .account(let account, _): + await DataSourceFacade.coordinateToProfileScene( + provider: self, + account: account + ) + case .notification: + assertionFailure("TODO") + case .hashtag(_): + assertionFailure("TODO") } } } From 9bf072e8273a380d5df93398b34838c7b51efc72 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 11 Jan 2024 14:07:57 +0100 Subject: [PATCH 113/159] Remove unused code --- .../NotificationView+Configuration.swift | 2 -- .../Entity/Mastodon+Entity+Account.swift | 27 ------------------- .../Content/NotificationView+ViewModel.swift | 1 - .../View/Content/NotificationView.swift | 2 -- 4 files changed, 32 deletions(-) diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index f03fc3fb7..d81ee425b 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -37,8 +37,6 @@ extension NotificationView { extension NotificationView { public func configure(notification: MastodonNotification) { - viewModel.objects.insert(notification) - configureAuthor(notification: notification) switch notification.entity.type { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index 9e5a39019..be4225659 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -51,33 +51,6 @@ extension Mastodon.Entity { public let source: Source? public let suspended: Bool? public let muteExpiresAt: Date? - - internal init(id: Mastodon.Entity.Account.ID, username: String, acct: String, url: String, displayName: String, note: String, avatar: String, avatarStatic: String? = nil, header: String, headerStatic: String? = nil, locked: Bool, emojis: [Mastodon.Entity.Emoji] = [], discoverable: Bool? = nil, createdAt: Date, lastStatusAt: Date? = nil, statusesCount: Int, followersCount: Int, followingCount: Int, moved: Mastodon.Entity.Account? = nil, fields: [Mastodon.Entity.Field]? = nil, bot: Bool? = nil, source: Mastodon.Entity.Source? = nil, suspended: Bool? = nil, muteExpiresAt: Date? = nil) { - self.id = id - self.username = username - self.acct = acct - self.url = url - self.displayName = displayName - self.note = note - self.avatar = avatar - self.avatarStatic = avatarStatic - self.header = header - self.headerStatic = headerStatic - self.locked = locked - self.emojis = emojis - self.discoverable = discoverable - self.createdAt = createdAt - self.lastStatusAt = lastStatusAt - self.statusesCount = statusesCount - self.followersCount = followersCount - self.followingCount = followingCount - self.moved = moved - self.fields = fields - self.bot = bot - self.source = source - self.suspended = suspended - self.muteExpiresAt = muteExpiresAt - } } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index ad59df742..a250028d5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -19,7 +19,6 @@ import CoreDataStack extension NotificationView { public final class ViewModel: ObservableObject { public var disposeBag = Set() - public var objects = Set() @Published public var context: AppContext? @Published public var authContext: AuthContext? diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 394904dc1..437a7bb2e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -178,8 +178,6 @@ public final class NotificationView: UIView { public func prepareForReuse() { disposeBag.removeAll() - - viewModel.objects.removeAll() viewModel.authContext = nil viewModel.authorAvatarImageURL = nil From c6788f5a434477b379f3cb0534dfa44a86cd4e5c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 11 Jan 2024 14:10:47 +0100 Subject: [PATCH 114/159] Split private/public into their own extension --- .../Persistence/FileManager+Timeline.swift | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift index 695686d6f..0dee5bf0c 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/FileManager+Timeline.swift @@ -3,24 +3,52 @@ import Foundation import MastodonSDK -extension FileManager { - private static let cacheItemsLimit: Int = 100 // max number of items to cache - +public extension FileManager { + // Retrieve - public func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] { + func cachedHomeTimeline(for userId: UserIdentifier) throws -> [MastodonStatus] { try cached(timeline: .homeTimeline(userId)).map(MastodonStatus.fromEntity) } - - public func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { + + func cachedNotificationsAll(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { try cached(timeline: .notificationsAll(userId)) } - - public func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { + + func cachedNotificationsMentions(for userId: UserIdentifier) throws -> [Mastodon.Entity.Notification] { try cached(timeline: .notificationsMentions(userId)) } - - private func cached(timeline: Persistence) throws -> [T] { + // Create + func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) { + cache(items.map { $0.entity }, timeline: .homeTimeline(userIdentifier)) + } + + func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { + cache(items, timeline: .notificationsAll(userIdentifier)) + } + + func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { + cache(items, timeline: .notificationsMentions(userIdentifier)) + } + + // Delete + func invalidateHomeTimelineCache(for userId: UserIdentifier) { + invalidate(timeline: .homeTimeline(userId)) + } + + func invalidateNotificationsAll(for userId: UserIdentifier) { + invalidate(timeline: .notificationsAll(userId)) + } + + func invalidateNotificationsMentions(for userId: UserIdentifier) { + invalidate(timeline: .notificationsMentions(userId)) + } +} + +private extension FileManager { + static let cacheItemsLimit: Int = 100 // max number of items to cache + + func cached(timeline: Persistence) throws -> [T] { guard let cachesDirectory else { return [] } let filePath = timeline.filepath(baseURL: cachesDirectory) @@ -36,20 +64,8 @@ extension FileManager { } } - // Create - public func cacheHomeTimeline(items: [MastodonStatus], for userIdentifier: UserIdentifier) { - cache(items.map { $0.entity }, timeline: .homeTimeline(userIdentifier)) - } - - public func cacheNotificationsAll(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { - cache(items, timeline: .notificationsAll(userIdentifier)) - } - - public func cacheNotificationsMentions(items: [Mastodon.Entity.Notification], for userIdentifier: UserIdentifier) { - cache(items, timeline: .notificationsMentions(userIdentifier)) - } - - private func cache(_ items: [T], timeline: Persistence) { + + func cache(_ items: [T], timeline: Persistence) { guard let cachesDirectory else { return } let processableItems: [T] @@ -68,21 +84,8 @@ extension FileManager { debugPrint(error.localizedDescription) } } - - // Delete - public func invalidateHomeTimelineCache(for userId: UserIdentifier) { - invalidate(timeline: .homeTimeline(userId)) - } - - public func invalidateNotificationsAll(for userId: UserIdentifier) { - invalidate(timeline: .notificationsAll(userId)) - } - - public func invalidateNotificationsMentions(for userId: UserIdentifier) { - invalidate(timeline: .notificationsMentions(userId)) - } - - private func invalidate(timeline: Persistence) { + + func invalidate(timeline: Persistence) { guard let cachesDirectory else { return } let filePath = timeline.filepath(baseURL: cachesDirectory) From b8ba69b930cc40d21e6a47edbfed007a5322a303 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 11 Jan 2024 15:28:18 +0100 Subject: [PATCH 115/159] Reduce nested-ness of tasks (IOS-192) Thank you @kimar --- .../Provider/DataSourceFacade+Profile.swift | 30 ++++++++----------- .../Login/MastodonLoginViewController.swift | 7 ++--- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index d0e38986b..ec91d9c82 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -56,22 +56,20 @@ extension DataSourceFacade { ) async { provider.coordinator.showLoading() - Task { - do { - guard let account = try await provider.context.apiService.fetchUser( - username: username, - domain: domain, - authenticationBox: provider.authContext.mastodonAuthenticationBox - ) else { - return provider.coordinator.hideLoading() - } - - provider.coordinator.hideLoading() - - await coordinateToProfileScene(provider: provider, account: account) - } catch { - provider.coordinator.hideLoading() + do { + guard let account = try await provider.context.apiService.fetchUser( + username: username, + domain: domain, + authenticationBox: provider.authContext.mastodonAuthenticationBox + ) else { + return provider.coordinator.hideLoading() } + + provider.coordinator.hideLoading() + + await coordinateToProfileScene(provider: provider, account: account) + } catch { + provider.coordinator.hideLoading() } } @@ -83,7 +81,6 @@ extension DataSourceFacade { ) async { provider.coordinator.showLoading() - Task { do { let account = try await provider.context.apiService.accountInfo( domain: domain, @@ -96,7 +93,6 @@ extension DataSourceFacade { await coordinateToProfileScene(provider: provider, account: account) } catch { provider.coordinator.hideLoading() - } } } diff --git a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift index 3d51393b9..fc35c6f83 100644 --- a/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift +++ b/Mastodon/Scene/Onboarding/Login/MastodonLoginViewController.swift @@ -142,13 +142,12 @@ class MastodonLoginViewController: UIViewController, NeedsDependency { authenticationViewModel .authenticated.sink { (domain, account) in - Task { + Task { @MainActor in do { _ = try await self.context.authenticationService.activeMastodonUser(domain: domain, userID: account.id) FileManager.default.store(account: account, forUserID: MastodonUserIdentifier(domain: domain, userID: account.id)) - Task { @MainActor in - self.coordinator.setup() - } + + self.coordinator.setup() } catch { assertionFailure(error.localizedDescription) } From 64dc97ab9bb50041146777248885a67dc1fcf73b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 11 Jan 2024 15:30:35 +0100 Subject: [PATCH 116/159] Use `guard` (IOS-192) --- Mastodon/Scene/Profile/ProfileViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 0d4ef4065..5b5be1cdc 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -221,7 +221,7 @@ extension ProfileViewController { } } - if let suspended = self.viewModel.account.suspended, suspended == true { + guard self.viewModel.account.suspended == false else { return } From cea61292294780f5ee3af4dcfbccd958393ccc49 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 11 Jan 2024 16:33:20 +0100 Subject: [PATCH 117/159] Download accounts at start and don't fail if there's no me (IOS-192) Known issue for know is that the profile-tab shows a blank screen. Still gotta fix that. --- .../Root/MainTab/MainTabBarController.swift | 59 ++++++++++--------- .../DataController/FeedDataController.swift | 1 + 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index a7681e82b..9012b7cad 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -89,38 +89,39 @@ class MainTabBarController: UITabBarController { @MainActor func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController { - guard let authContext, let me = authContext.mastodonAuthenticationBox.authentication.account() else { - return UITableViewController() - } + guard let authContext else { return UITableViewController() } let viewController: UIViewController switch self { - case .home: - let _viewController = HomeTimelineViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .search: - let _viewController = SearchViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .compose: - viewController = UIViewController() - case .notifications: - let _viewController = NotificationViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .me: - let _viewController = ProfileViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me) - viewController = _viewController + case .home: + let _viewController = HomeTimelineViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext) + viewController = _viewController + case .search: + let _viewController = SearchViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) + viewController = _viewController + case .compose: + viewController = UIViewController() + case .notifications: + let _viewController = NotificationViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = .init(context: context, authContext: authContext) + viewController = _viewController + case .me: + #warning("What happens if there's no me at the beginning? I guess we _do_ need another migration?") + guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return UIViewController() } + + let _viewController = ProfileViewController() + _viewController.context = context + _viewController.coordinator = coordinator + _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me) + viewController = _viewController } viewController.title = self.title return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) diff --git a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift index 46183ec25..b013e0560 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift @@ -77,6 +77,7 @@ private extension FeedDataController { 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: From 2c653320fbfed169fef2d4a6c9321a6bbe3b705e Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 11 Jan 2024 23:36:13 +0100 Subject: [PATCH 118/159] Refactor tab/MainTabBarController to add viewcontrollers as properties This is a WIP-step for account-stuff. Also: iPhone only, iPad should come next --- Mastodon.xcodeproj/project.pbxproj | 4 + Mastodon/Coordinator/SceneCoordinator.swift | 2 +- .../Root/ContentSplitViewController.swift | 8 +- .../Root/MainTab/MainTabBarController.swift | 190 +++++------------- .../Scene/Root/RootSplitViewController.swift | 10 +- .../Root/Sidebar/SidebarViewController.swift | 2 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 8 +- Mastodon/Scene/Root/Tab.swift | 111 ++++++++++ 8 files changed, 180 insertions(+), 155 deletions(-) create mode 100644 Mastodon/Scene/Root/Tab.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 02070824f..66cca4dd3 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; }; D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */; }; D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; }; + D8CF45832B50893900C84D70 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CF45822B50893900C84D70 /* Tab.swift */; }; D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; }; D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; }; D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; }; @@ -833,6 +834,7 @@ D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewModel.swift; sourceTree = ""; }; D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = ""; }; + D8CF45822B50893900C84D70 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = ""; }; D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = ""; }; D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = ""; }; @@ -2557,6 +2559,7 @@ DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */, DB852D1A26FAED0100FC9D81 /* Sidebar */, DB8AF54E25C13703002E6C99 /* MainTab */, + D8CF45822B50893900C84D70 /* Tab.swift */, ); path = Root; sourceTree = ""; @@ -3970,6 +3973,7 @@ 2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */, C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */, DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */, + D8CF45832B50893900C84D70 /* Tab.swift in Sources */, D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */, DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */, DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index ef7f3b9cf..f2c7d4c6f 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -387,7 +387,7 @@ extension SceneCoordinator { return viewController } - func switchToTabBar(tab: MainTabBarController.Tab) { + func switchToTabBar(tab: Tab) { splitViewController?.contentSplitViewController.currentSupplementaryTab = tab splitViewController?.compactMainTabBarViewController.selectedIndex = tab.rawValue diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index fdc5b5ba3..68294739e 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -11,8 +11,8 @@ import CoreDataStack import MastodonCore protocol ContentSplitViewControllerDelegate: AnyObject { - func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) - func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: MainTabBarController.Tab) + func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: Tab) + func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: Tab) } final class ContentSplitViewController: UIViewController, NeedsDependency { @@ -37,7 +37,7 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { return sidebarViewController }() - @Published var currentSupplementaryTab: MainTabBarController.Tab = .home + @Published var currentSupplementaryTab: Tab = .home private(set) lazy var mainTabBarController: MainTabBarController = { let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator, authContext: authContext) if let homeTimelineViewController = mainTabBarController.viewController(of: HomeTimelineViewController.self) { @@ -102,7 +102,7 @@ extension ContentSplitViewController { // MARK: - SidebarViewControllerDelegate extension ContentSplitViewController: SidebarViewControllerDelegate { - func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) { + func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: Tab) { delegate?.contentSplitViewController(self, sidebarViewController: sidebarViewController, didSelectTab: tab) } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 9012b7cad..6a3c54fb2 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -34,102 +34,13 @@ class MainTabBarController: UITabBarController { ) @Published var currentTab: Tab = .home - - enum Tab: Int, CaseIterable { - case home - case search - case compose - case notifications - case me - var tag: Int { - return rawValue - } - - var title: String { - switch self { - case .home: return L10n.Common.Controls.Tabs.home - case .search: return L10n.Common.Controls.Tabs.searchAndExplore - case .compose: return L10n.Common.Controls.Actions.compose - case .notifications: return L10n.Common.Controls.Tabs.notifications - case .me: return L10n.Common.Controls.Tabs.profile - } - } + let homeTimelineViewController: HomeTimelineViewController + let searchViewController: SearchViewController + let composeViewController: UIViewController // placeholder + let notificationViewController: NotificationViewController + let meProfileViewController: ProfileViewController - var inputLabels: [String]? { - switch self { - case .home, .compose, .notifications, .me: - return nil - case .search: - return [ - L10n.Common.Controls.Tabs.A11Y.search, - L10n.Common.Controls.Tabs.A11Y.explore, - L10n.Common.Controls.Tabs.searchAndExplore - ] - } - } - - var image: UIImage { - switch self { - case .home: return UIImage(systemName: "house")! - case .search: return UIImage(systemName: "magnifyingglass")! - case .compose: return UIImage(systemName: "square.and.pencil")! - case .notifications: return UIImage(systemName: "bell")! - case .me: return UIImage(systemName: "person")! - } - } - - var selectedImage: UIImage { - return image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal) - } - - var largeImage: UIImage { - return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80)) - } - - @MainActor - func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController { - guard let authContext else { return UITableViewController() } - - let viewController: UIViewController - switch self { - case .home: - let _viewController = HomeTimelineViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext) - viewController = _viewController - case .search: - let _viewController = SearchViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .compose: - viewController = UIViewController() - case .notifications: - let _viewController = NotificationViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = .init(context: context, authContext: authContext) - viewController = _viewController - case .me: - #warning("What happens if there's no me at the beginning? I guess we _do_ need another migration?") - guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return UIViewController() } - - let _viewController = ProfileViewController() - _viewController.context = context - _viewController.coordinator = coordinator - _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me) - viewController = _viewController - } - viewController.title = self.title - return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) - } - } - - var _viewControllers: [UIViewController] = [] - private(set) var isReadyForWizardAvatarButton = false // output @@ -146,15 +57,43 @@ class MainTabBarController: UITabBarController { self.context = context self.coordinator = coordinator self.authContext = authContext + + homeTimelineViewController = HomeTimelineViewController() + homeTimelineViewController.configureTabBarItem(with: .home) + homeTimelineViewController.context = context + homeTimelineViewController.coordinator = coordinator + + searchViewController = SearchViewController() + searchViewController.configureTabBarItem(with: .search) + searchViewController.context = context + searchViewController.coordinator = coordinator + + composeViewController = UIViewController() + composeViewController.configureTabBarItem(with: .compose) + + notificationViewController = NotificationViewController() + notificationViewController.configureTabBarItem(with: .notifications) + notificationViewController.context = context + notificationViewController.coordinator = coordinator + + meProfileViewController = ProfileViewController() + meProfileViewController.context = context + meProfileViewController.coordinator = coordinator + meProfileViewController.configureTabBarItem(with: .me) + + if let authContext { + notificationViewController.viewModel = NotificationViewModel(context: context, authContext: authContext) + homeTimelineViewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext) + searchViewController.viewModel = SearchViewModel(context: context, authContext: authContext) + } + super.init(nibName: nil, bundle: nil) + + viewControllers = [homeTimelineViewController, searchViewController, composeViewController, notificationViewController, meProfileViewController].map { AdaptiveStatusBarStyleNavigationController(rootViewController: $0) } tabBar.addInteraction(largeContentViewerInteraction) - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension MainTabBarController { @@ -171,26 +110,9 @@ extension MainTabBarController { view.backgroundColor = .systemBackground // seealso: `ThemeService.apply(theme:)` - 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 }) { @@ -201,7 +123,7 @@ extension MainTabBarController { context.apiService.error .receive(on: DispatchQueue.main) .sink { [weak self] error in - guard let self = self, let coordinator = self.coordinator else { return } + guard let self, let coordinator = self.coordinator else { return } switch error { case .implicit: break @@ -228,15 +150,14 @@ extension MainTabBarController { ) .receive(on: DispatchQueue.main) .sink { [weak self] authentication, currentTab in - guard let self = self else { return } - guard let notificationViewController = self.notificationViewController else { return } - + guard let self 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]) @@ -244,17 +165,18 @@ extension MainTabBarController { } 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 } + guard let self else { return } self.avatarButton.avatarImageView.setImage( url: avatarURL, placeholder: .placeholder(color: .systemFill), @@ -262,7 +184,7 @@ extension MainTabBarController { ) } .store(in: &disposeBag) - + NotificationCenter.default.publisher(for: .userFetched) .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -300,11 +222,11 @@ extension MainTabBarController { $currentTab .receive(on: DispatchQueue.main) .sink { [weak self] tab in - guard let self = self else { return } + guard let self else { return } self.updateAvatarButtonAppearance() } .store(in: &disposeBag) - + updateTabBarDisplay() } @@ -358,7 +280,7 @@ extension MainTabBarController { case .search: assert(Thread.isMainThread) // double tapping search tab opens the search bar without additional taps - searchViewController?.searchBar.becomeFirstResponder() + searchViewController.searchBar.becomeFirstResponder() default: break } @@ -460,18 +382,6 @@ extension MainTabBarController { } } -extension MainTabBarController { - - var notificationViewController: NotificationViewController? { - return viewController(of: NotificationViewController.self) - } - - var searchViewController: SearchViewController? { - return viewController(of: SearchViewController.self) - } - -} - // MARK: - UITabBarControllerDelegate extension MainTabBarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index ea87f2500..8a6a88dfd 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -127,8 +127,8 @@ extension RootSplitViewController { // MARK: - ContentSplitViewControllerDelegate extension RootSplitViewController: ContentSplitViewControllerDelegate { - func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) { - guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { + func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: Tab) { + guard let _ = Tab.allCases.firstIndex(of: tab) else { assertionFailure() return } @@ -158,8 +158,8 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate { } } - func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: MainTabBarController.Tab) { - guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { + func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: Tab) { + guard let _ = Tab.allCases.firstIndex(of: tab) else { assertionFailure() return } @@ -170,7 +170,7 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate { guard !isPrimaryDisplay else { return } - contentSplitViewController.mainTabBarController.searchViewController?.searchBar.becomeFirstResponder() + contentSplitViewController.mainTabBarController.searchViewController.searchBar.becomeFirstResponder() default: break } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index b9a7d80f1..e13fa8443 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -12,7 +12,7 @@ import MastodonCore import MastodonUI protocol SidebarViewControllerDelegate: AnyObject { - func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) + func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: Tab) func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView) func sidebarViewController(_ sidebarViewController: SidebarViewController, didDoubleTapItem item: SidebarViewModel.Item, sourceView: UIView) } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 84b7f1cba..85c47fc42 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -23,7 +23,7 @@ final class SidebarViewModel { let authContext: AuthContext? @Published private var isSidebarDataSourceReady = false @Published private var isAvatarButtonDataReady = false - @Published var currentTab: MainTabBarController.Tab = .home + @Published var currentTab: Tab = .home // output var diffableDataSource: UICollectionViewDiffableDataSource? @@ -57,7 +57,7 @@ extension SidebarViewModel { } enum Item: Hashable { - case tab(MainTabBarController.Tab) + case tab(Tab) case setting case compose } @@ -69,7 +69,7 @@ extension SidebarViewModel { collectionView: UICollectionView, secondaryCollectionView: UICollectionView ) { - let tabCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in + let tabCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in guard let self else { return } let imageURL: URL? @@ -125,7 +125,7 @@ extension SidebarViewModel { let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor]) image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)! } else { - image = MainTabBarController.Tab.notifications.image + image = Tab.notifications.image } cell.item?.image = image cell.item?.activeImage = image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal) diff --git a/Mastodon/Scene/Root/Tab.swift b/Mastodon/Scene/Root/Tab.swift new file mode 100644 index 000000000..a3303ba4e --- /dev/null +++ b/Mastodon/Scene/Root/Tab.swift @@ -0,0 +1,111 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import UIKit +import MastodonLocalization +import MastodonAsset + +enum Tab: Int, CaseIterable { + case home + case search + case compose + case notifications + case me + + var tag: Int { + return rawValue + } + + var title: String { + switch self { + case .home: return L10n.Common.Controls.Tabs.home + case .search: return L10n.Common.Controls.Tabs.searchAndExplore + case .compose: return L10n.Common.Controls.Actions.compose + case .notifications: return L10n.Common.Controls.Tabs.notifications + case .me: return L10n.Common.Controls.Tabs.profile + } + } + + var inputLabels: [String]? { + switch self { + case .home, .compose, .notifications, .me: + return nil + case .search: + return [ + L10n.Common.Controls.Tabs.A11Y.search, + L10n.Common.Controls.Tabs.A11Y.explore, + L10n.Common.Controls.Tabs.searchAndExplore + ] + } + } + + var image: UIImage { + switch self { + case .home: return UIImage(systemName: "house")! + case .search: return UIImage(systemName: "magnifyingglass")! + case .compose: return UIImage(systemName: "square.and.pencil")! + case .notifications: return UIImage(systemName: "bell")! + case .me: return UIImage(systemName: "person")! + } + } + + var selectedImage: UIImage { + return image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal) + } + + var largeImage: UIImage { + return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80)) + } + +// @MainActor +// func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController { +// guard let authContext else { return UITableViewController() } +// +// let viewController: UIViewController +// switch self { +// case .home: +// let _viewController = HomeTimelineViewController() +// _viewController.context = context +// _viewController.coordinator = coordinator +// _viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext) +// viewController = _viewController +// case .search: +// let _viewController = SearchViewController() +// _viewController.context = context +// _viewController.coordinator = coordinator +// _viewController.viewModel = SearchViewModel(context: context, authContext: authContext) +// viewController = _viewController +// case .compose: +// viewController = UIViewController() +// case .notifications: +// let _viewController = NotificationViewController() +// _viewController.context = context +// _viewController.coordinator = coordinator +// _viewController.viewModel = NotificationViewModel(context: context, authContext: authContext) +// viewController = _viewController +// case .me: +// #warning("What happens if there's no me at the beginning? I guess we _do_ need another migration?") +// guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return UIViewController() } +// +// let _viewController = ProfileViewController() +// _viewController.context = context +// _viewController.coordinator = coordinator +// _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me) +// viewController = _viewController +// } +// viewController.title = self.title +// return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) +// } +} + +extension UIViewController { + func configureTabBarItem(with tab: Tab) { + title = tab.title + tabBarItem.tag = tab.tag + tabBarItem.title = tab.title // needs for acessiblity large content label + tabBarItem.image = tab.image.imageWithoutBaseline() + tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline() + tabBarItem.accessibilityLabel = tab.title + tabBarItem.accessibilityUserInputLabels = tab.inputLabels + tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) + } +} From 127c3167b8406c3ce321e36388aacbf88b8df1d7 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 12 Jan 2024 10:47:17 +0100 Subject: [PATCH 119/159] Fix profile-button (IOS-192) --- .../Root/MainTab/MainTabBarController.swift | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 6a3c54fb2..2a84c02ef 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -91,6 +91,8 @@ class MainTabBarController: UITabBarController { viewControllers = [homeTimelineViewController, searchViewController, composeViewController, notificationViewController, meProfileViewController].map { AdaptiveStatusBarStyleNavigationController(rootViewController: $0) } tabBar.addInteraction(largeContentViewerInteraction) + + layoutAvatarButton() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -171,8 +173,6 @@ extension MainTabBarController { } .store(in: &disposeBag) - layoutAvatarButton() - $avatarURL .receive(on: DispatchQueue.main) .sink { [weak self] avatarURL in @@ -188,21 +188,24 @@ extension MainTabBarController { 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) - } + guard let self, + let authContext = self.authContext, + let account = authContext.mastodonAuthenticationBox.authentication.account() else { return } + + 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) + + self.meProfileViewController.viewModel = ProfileViewModel(context: self.context, authContext: authContext, account: account, relationship: nil, me: account) } .store(in: &disposeBag) @@ -315,8 +318,7 @@ extension MainTabBarController { private func layoutAvatarButton() { guard avatarButton.superview == nil else { return } - let _profileTabItem = self.tabBar.items?.first { item in item.tag == Tab.me.tag } - guard let profileTabItem = _profileTabItem else { return } + guard let profileTabItem = meProfileViewController.tabBarItem else { return } guard let view = profileTabItem.value(forKey: "view") as? UIView else { return } From 35c017986a584e193d6baa153871cb3ba0bdeb8a Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 16 Jan 2024 15:17:09 +0100 Subject: [PATCH 120/159] [WIP] Add relationships/user to notifications (IOS-192) --- ...er+NotificationTableViewCellDelegate.swift | 1 + .../NotificationTableViewCell+ViewModel.swift | 2 +- ...ineViewController+DataSourceProvider.swift | 6 +- .../NotificationTimelineViewModel.swift | 9 +- .../NotificationView+Configuration.swift | 139 +++++------------- .../DataController/FeedDataController.swift | 40 +++-- .../Sources/MastodonSDK/MastodonFeed.swift | 8 +- .../MastodonSDK/MastodonNotification.swift | 16 +- 8 files changed, 89 insertions(+), 132 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 700105814..850a240db 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -31,6 +31,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } + //TODO: Update Relationship _ = try await DataSourceFacade.responseToMenuAction( dependency: self, action: action, diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift index 135a7adc2..c45d722e4 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift @@ -57,7 +57,7 @@ extension NotificationTableViewCell { UIView.performWithoutAnimation { tableView.beginUpdates() - tableView.endUpdates() + tableView.endUpdates() } } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index fb83eb380..f23308cb4 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -22,10 +22,12 @@ extension NotificationTimelineViewController: DataSourceProvider { switch item { case .feed(let feed): - let managedObjectContext = context.managedObjectContext let item: DataSourceItem? = { guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil } - if let notification = feed.notification, let mastodonNotification = MastodonNotification.fromEntity(notification, using: managedObjectContext, domain: authContext.mastodonAuthenticationBox.domain) { + + //TODO: Get relationship + if let notification = feed.notification, + let mastodonNotification = MastodonNotification.fromEntity(notification, relationship: nil, domain: authContext.mastodonAuthenticationBox.domain) { return .notification(record: mastodonNotification) } else { return nil diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index a3bcbb877..e6a48d092 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -53,18 +53,19 @@ final class NotificationTimelineViewModel { self.authContext = authContext self.scope = scope self.dataController = FeedDataController(context: context, authContext: authContext) - + switch scope { case .everything: + //TODO: I need the relationship here, too self.dataController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox))?.map({ notification in - MastodonFeed.fromNotification(notification, kind: .notificationAll) + MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationAll) }) ?? [] case .mentions: self.dataController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox))?.map({ notification in - MastodonFeed.fromNotification(notification, kind: .notificationMentions) + MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationMentions) }) ?? [] } - + self.dataController.$records .removeDuplicates() .receive(on: DispatchQueue.main) diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index d81ee425b..1f77a2d33 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -19,17 +19,14 @@ import MastodonSDK extension NotificationView { public func configure(feed: MastodonFeed) { - guard - let notification = feed.notification, - let managedObjectContext = viewModel.context?.managedObjectContext - else { + guard let notification = feed.notification else { assertionFailure() return } MastodonNotification.fromEntity( notification, - using: managedObjectContext, + relationship: feed.relationship, domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? "" ).map(configure(notification:)) } @@ -65,56 +62,29 @@ extension NotificationView { extension NotificationView { private func configureAuthor(notification: MastodonNotification) { let author = notification.account + // author avatar - - Publishers.CombineLatest( - author.publisher(for: \.avatar), - UserDefaults.shared.publisher(for: \.preferredStaticAvatar) - ) - .map { _ in author.avatarImageURL() } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) - + viewModel.authorAvatarImageURL = author.avatarImageURL() + // author name - Publishers.CombineLatest( - author.publisher(for: \.displayName), - author.publisher(for: \.emojis) - ) - .map { _, emojis in - do { - let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure(error.localizedDescription) - return PlaintextMetaContent(string: author.displayNameWithFallback) - } + do { + let content = MastodonContent(content: author.displayNameWithFallback, emojis: author.emojis.asDictionary) + viewModel.authorName = try MastodonMetaContent.convert(document: content) + } catch { + assertionFailure(error.localizedDescription) + viewModel.authorName = PlaintextMetaContent(string: author.displayNameWithFallback) } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) - // author username - author.publisher(for: \.acct) - .map { $0 as String? } - .assign(to: \.authorUsername, on: viewModel) - .store(in: &disposeBag) - // timestamp + + viewModel.authorUsername = author.acct viewModel.timestamp = notification.entity.createdAt viewModel.visibility = notification.entity.status?.mastodonVisibility ?? ._other("") // notification type indicator - Publishers.CombineLatest( - author.publisher(for: \.displayName), - author.publisher(for: \.emojis) - ) - .sink { [weak self] _, emojis in - guard let self = self else { return } - guard let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) else { - self.viewModel.notificationIndicatorText = nil - return - } + if let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) { self.viewModel.type = type + // TODO: fix the i18n. The subject should assert place at the string beginning func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent { let content = MastodonContent(content: text, emojis: emojis) guard let metaContent = try? MastodonMetaContent.convert(document: content) else { @@ -122,102 +92,65 @@ extension NotificationView { } return metaContent } - - // TODO: fix the i18n. The subject should assert place at the string beginning + switch type { case .follow: self.viewModel.notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.followedYou, - emojis: emojis.asDictionary + emojis: author.emojis.asDictionary ) case .followRequest: self.viewModel.notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou, - emojis: emojis.asDictionary + emojis: author.emojis.asDictionary ) case .mention: self.viewModel.notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.mentionedYou, - emojis: emojis.asDictionary + emojis: author.emojis.asDictionary ) case .reblog: self.viewModel.notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost, - emojis: emojis.asDictionary + emojis: author.emojis.asDictionary ) case .favourite: self.viewModel.notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost, - emojis: emojis.asDictionary + emojis: author.emojis.asDictionary ) case .poll: self.viewModel.notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.pollHasEnded, - emojis: emojis.asDictionary + emojis: author.emojis.asDictionary ) case .status: self.viewModel.notificationIndicatorText = createMetaContent( text: .empty, - emojis: emojis.asDictionary + emojis: author.emojis.asDictionary ) case ._other: self.viewModel.notificationIndicatorText = nil } + } else { + self.viewModel.notificationIndicatorText = nil } - .store(in: &disposeBag) - let authContext = viewModel.authContext - // isMuting - author.publisher(for: \.mutingBy) - .map { mutingBy in - guard let authContext = authContext else { return false } - return mutingBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID - && $0.domain == authContext.mastodonAuthenticationBox.domain - }) + if let me = viewModel.authContext?.mastodonAuthenticationBox.authentication.account() { + viewModel.isMyself = (author == me) + + if let relationship = notification.relationship { + viewModel.isMuting = relationship.muting + viewModel.isBlocking = relationship.blocking || relationship.domainBlocking + viewModel.isFollowed = relationship.following + } else { + viewModel.isMuting = false + viewModel.isBlocking = false + viewModel.isFollowed = false } - .assign(to: \.isMuting, on: viewModel) - .store(in: &disposeBag) - // isBlocking - author.publisher(for: \.blockingBy) - .map { blockingBy in - guard let authContext = authContext else { return false } - return blockingBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID - && $0.domain == authContext.mastodonAuthenticationBox.domain - }) - } - .assign(to: \.isBlocking, on: viewModel) - .store(in: &disposeBag) - - // isMyself - Publishers.CombineLatest( - author.publisher(for: \.domain), - author.publisher(for: \.id) - ) - .map { domain, id in - guard let authContext = authContext else { return false } - return authContext.mastodonAuthenticationBox.domain == domain - && authContext.mastodonAuthenticationBox.userID == id } - .assign(to: \.isMyself, on: viewModel) - .store(in: &disposeBag) - // follow request state viewModel.followRequestState = notification.followRequestState viewModel.transientFollowRequestState = notification.transientFollowRequestState - - // Following - author.publisher(for: \.followingBy) - .map { [weak viewModel] followingBy in - guard let viewModel = viewModel else { return false } - guard let authContext = viewModel.authContext else { return false } - return followingBy.contains(where: { - $0.id == authContext.mastodonAuthenticationBox.userID && $0.domain == authContext.mastodonAuthenticationBox.domain - }) - } - .assign(to: \.isFollowed, on: viewModel) - .store(in: &disposeBag) - } } diff --git a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift index b013e0560..ebc511d4d 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift @@ -76,16 +76,36 @@ final public class FeedDataController { private extension FeedDataController { 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) } + 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 getFeeds(with: .everything) + case .notificationMentions: + return try await getFeeds(with: .mentions) } } + + private func getFeeds(with scope: APIService.MastodonNotificationScope) async throws -> [MastodonFeed] { + + let notifications = try await context.apiService.notifications(maxID: nil, scope: scope, authenticationBox: authContext.mastodonAuthenticationBox).value + + let accounts = notifications.map { $0.account } + let relationships = try await context.apiService.relationship(forAccounts: accounts, authenticationBox: authContext.mastodonAuthenticationBox).value + + let notificationsWithRelationship: [(notification: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?)] = notifications.compactMap { notification in + guard let relationship = relationships.first(where: {$0.id == notification.account.id }) else { return (notification: notification, relationship: nil)} + + return (notification: notification, relationship: relationship) + } + + let feeds = notificationsWithRelationship.compactMap({ (notification: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?) in + MastodonFeed.fromNotification(notification, relationship: relationship, kind: .notificationAll) + }) + + return feeds + } + } + diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index fbeed892f..96d78fb46 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -16,16 +16,18 @@ public final class MastodonFeed { public var isLoadingMore: Bool = false public let status: MastodonStatus? + public let relationship: Mastodon.Entity.Relationship? public let notification: Mastodon.Entity.Notification? public let kind: Feed.Kind - init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, kind: Feed.Kind) { + init(hasMore: Bool, isLoadingMore: Bool, status: MastodonStatus?, notification: Mastodon.Entity.Notification?, relationship: Mastodon.Entity.Relationship?, kind: Feed.Kind) { self.id = notification?.id ?? status?.id ?? UUID().uuidString self.hasMore = hasMore self.isLoadingMore = isLoadingMore self.status = status self.notification = notification + self.relationship = relationship self.kind = kind } } @@ -37,11 +39,12 @@ public extension MastodonFeed { isLoadingMore: false, status: status, notification: nil, + relationship: nil, kind: kind ) } - static func fromNotification(_ notification: Mastodon.Entity.Notification, kind: Feed.Kind) -> MastodonFeed { + static func fromNotification(_ notification: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?, kind: Feed.Kind) -> MastodonFeed { MastodonFeed( hasMore: false, isLoadingMore: false, @@ -52,6 +55,7 @@ public extension MastodonFeed { return .fromEntity(status) }(), notification: notification, + relationship: relationship, kind: kind ) } diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift index b32d59c29..fcedbbbe2 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift @@ -10,30 +10,26 @@ public final class MastodonNotification { entity.id } - public let account: MastodonUser + public let account: Mastodon.Entity.Account + public let relationship: Mastodon.Entity.Relationship? public let status: MastodonStatus? public let feeds: [MastodonFeed] public var followRequestState: MastodonFollowRequestState = .init(state: .none) public var transientFollowRequestState: MastodonFollowRequestState = .init(state: .none) - public init(entity: Mastodon.Entity.Notification, account: MastodonUser, status: MastodonStatus?, feeds: [MastodonFeed]) { + public init(entity: Mastodon.Entity.Notification, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, status: MastodonStatus?, feeds: [MastodonFeed]) { self.entity = entity self.account = account + self.relationship = relationship self.status = status self.feeds = feeds } } public extension MastodonNotification { - static func fromEntity(_ entity: Mastodon.Entity.Notification, using managedObjectContext: NSManagedObjectContext, domain: String) -> MastodonNotification? { - guard let user = MastodonUser.fetch(in: managedObjectContext, configurationBlock: { request in - request.predicate = MastodonUser.predicate(domain: domain, id: entity.account.id) - }).first else { - assertionFailure() - return nil - } - return MastodonNotification(entity: entity, account: user, status: entity.status.map(MastodonStatus.fromEntity), feeds: []) + static func fromEntity(_ entity: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?, domain: String) -> MastodonNotification? { + return MastodonNotification(entity: entity, account: entity.account, relationship: relationship, status: entity.status.map(MastodonStatus.fromEntity), feeds: []) } } From ca3fd69b62f93f835c611e780de70e2c4a331262 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 18 Jan 2024 16:59:14 +0100 Subject: [PATCH 121/159] [WIP] Preparation to get relationship into menu on notification-screen (IOS-192) --- ...er+NotificationTableViewCellDelegate.swift | 45 ++++++++++++++----- .../View/Content/NotificationView.swift | 1 + 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 850a240db..7d41d27ff 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -31,18 +31,41 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } - //TODO: Update Relationship - _ = try await DataSourceFacade.responseToMenuAction( - dependency: self, - action: action, - menuContext: .init( - author: notification.entity.account, - statusViewModel: nil, - button: button, - barButtonItem: nil + // we only allow to mute/block and to report users on notification-screen + switch action { + case .muteUser(_), .blockUser(_): + _ = try await DataSourceFacade.responseToMenuAction( + dependency: self, + action: action, + menuContext: .init( + author: notification.entity.account, + statusViewModel: nil, + button: button, + barButtonItem: nil + ), + completion: { (newRelationship: Mastodon.Entity.Relationship) in + //TODO: Update Relationship. + //TODO: Get Relationship into here, first! + + print(newRelationship) + } ) - ) - } // end Task + case .reportUser(_): + _ = try await DataSourceFacade.responseToMenuAction( + dependency: self, + action: action, + menuContext: .init( + author: notification.entity.account, + statusViewModel: nil, + button: button, + barButtonItem: nil + ) + ) + case .translateStatus(_), .showOriginal, .shareUser(_), .blockDomain(_), .bookmarkStatus(_), .hideReblogs(_), .shareStatus, .deleteStatus, .editStatus, .followUser(_): + // Do Nothing + break + } + } } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 437a7bb2e..da257f0c0 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -476,6 +476,7 @@ extension NotificationView: AdaptiveContainerView { } extension NotificationView { + @available(*, deprecated, message: "Does way too much") public typealias AuthorMenuContext = StatusAuthorView.AuthorMenuContext public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) { From 6ffcdb256547607a2ca62b3b75a05a6e5dda8994 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 20 Jan 2024 14:05:04 +0100 Subject: [PATCH 122/159] Remove unused property (IOS-192) --- .../Content/NotificationView+ViewModel.swift | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift index a250028d5..e0eb3e312 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -26,7 +26,6 @@ extension NotificationView { @Published public var type: MastodonNotificationType? @Published public var notificationIndicatorText: MetaContent? - @Published public var authorAvatarImage: UIImage? @Published public var authorAvatarImageURL: URL? @Published public var authorName: MetaContent? @Published public var authorUsername: String? @@ -70,22 +69,14 @@ extension NotificationView.ViewModel { private func bindAuthor(notificationView: NotificationView) { // avatar - Publishers.CombineLatest( - $authorAvatarImage, - $authorAvatarImageURL - ) - .sink { image, url in - let configuration: AvatarImageView.Configuration = { - if let image = image { - return AvatarImageView.Configuration(image: image) - } else { - return AvatarImageView.Configuration(url: url) - } - }() - notificationView.avatarButton.avatarImageView.configure(configuration: configuration) - notificationView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) - } - .store(in: &disposeBag) + + $authorAvatarImageURL + .sink { url in + let configuration = AvatarImageView.Configuration(url: url) + notificationView.avatarButton.avatarImageView.configure(configuration: configuration) + notificationView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) + } + .store(in: &disposeBag) // name $authorName .sink { metaContent in @@ -158,11 +149,8 @@ extension NotificationView.ViewModel { } .store(in: &disposeBag) - Publishers.CombineLatest( - $authorAvatarImage, $type - ) - .sink { avatarImage, type in + .sink { type in var actions = [UIAccessibilityCustomAction]() // these notifications can be directly actioned to view the profile @@ -170,7 +158,7 @@ extension NotificationView.ViewModel { actions.append( UIAccessibilityCustomAction( name: L10n.Common.Controls.Status.showUserProfile, - image: avatarImage + image: nil ) { [weak notificationView] _ in guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false } delegate.notificationView(notificationView, authorAvatarButtonDidPressed: notificationView.avatarButton) From 484f44e17c90a194615546abf03e3e0758ba20e6 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 20 Jan 2024 14:18:26 +0100 Subject: [PATCH 123/159] Remove code (IOS-192) --- .../Scene/Profile/ProfileViewController.swift | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 5b5be1cdc..f3999bff6 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -852,28 +852,11 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { ) self.viewModel.relationship = newRelationship - // update account? - // update me? + // TODO: update account? + // TODO: update me? } } - - // switch relationshipAction { - // case .none: - // break - // case .follow, .request, .pending, .following: - // guard let user = viewModel.user else { return } - // let record = ManagedObjectRecord(objectID: user.objectID) - // Task { - // try await DataSourceFacade.responseToUserFollowAction( - // dependency: self, - // user: record - // ) - // } - // case .muting: - // case .blocked, .showReblogs, .isMyself,.followingBy, .blockingBy, .suspended, .edit, .editing, .updating: - // break - // } } func profileHeaderViewController( From ab2f54307fe2164eb43ef54cbac7502b3da913a1 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 20 Jan 2024 18:21:02 +0100 Subject: [PATCH 124/159] Group files and use simpler context (IOS-192) --- Mastodon.xcodeproj/project.pbxproj | 18 +++++++++++- .../NotificationView+Configuration.swift | 0 .../NotificationView+ViewModel.swift | 29 ++++--------------- .../NotificationView}/NotificationView.swift | 10 +++++-- 4 files changed, 30 insertions(+), 27 deletions(-) rename Mastodon/Scene/{Share/View/Content => Notification/NotificationView}/NotificationView+Configuration.swift (100%) rename {MastodonSDK/Sources/MastodonUI/View/Content => Mastodon/Scene/Notification/NotificationView}/NotificationView+ViewModel.swift (90%) rename {MastodonSDK/Sources/MastodonUI/View/Content => Mastodon/Scene/Notification/NotificationView}/NotificationView.swift (99%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 66cca4dd3..e713b0afe 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -132,6 +132,8 @@ D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099077294BC8A30050219F /* PrivacyTableViewController.swift */; }; D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; }; D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; }; + D80F627C2B5C32C500877059 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80F627A2B5C32C500877059 /* NotificationView.swift */; }; + D80F627D2B5C32C500877059 /* NotificationView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80F627B2B5C32C500877059 /* NotificationView+ViewModel.swift */; }; D81439862AD415DE0071A88F /* AboutInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81439852AD415DE0071A88F /* AboutInstance.swift */; }; D81439882AD450A40071A88F /* AboutInstanceTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81439872AD450A40071A88F /* AboutInstanceTableViewDataSource.swift */; }; D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */; }; @@ -784,6 +786,8 @@ D8099077294BC8A30050219F /* PrivacyTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewController.swift; sourceTree = ""; }; D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = ""; }; D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = ""; }; + D80F627A2B5C32C500877059 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; + D80F627B2B5C32C500877059 /* NotificationView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationView+ViewModel.swift"; sourceTree = ""; }; D81439852AD415DE0071A88F /* AboutInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstance.swift; sourceTree = ""; }; D81439872AD450A40071A88F /* AboutInstanceTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstanceTableViewDataSource.swift; sourceTree = ""; }; D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverviewTableViewController.swift; sourceTree = ""; }; @@ -1540,7 +1544,6 @@ children = ( DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */, DB0FCB992797F7AD006C02E2 /* UserView+Configuration.swift */, - DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */, 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */, 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */, DBE3CDCE261C42ED00430CC6 /* TimelineHeaderView.swift */, @@ -1817,6 +1820,16 @@ path = Privacy; sourceTree = ""; }; + D80F627E2B5C32E400877059 /* NotificationView */ = { + isa = PBXGroup; + children = ( + D80F627A2B5C32C500877059 /* NotificationView.swift */, + D80F627B2B5C32C500877059 /* NotificationView+ViewModel.swift */, + DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */, + ); + path = NotificationView; + sourceTree = ""; + }; D81A22732AB4641F00905D71 /* Search Results Overview */ = { isa = PBXGroup; children = ( @@ -2731,6 +2744,7 @@ children = ( DB63F765279A5E5600455B82 /* NotificationTimeline */, 2D35237F26256F470031AF25 /* Cell */, + D80F627E2B5C32E400877059 /* NotificationView */, DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */, 2D607AD726242FC500B70763 /* NotificationViewModel.swift */, ); @@ -3782,6 +3796,7 @@ DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */, DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */, + D80F627C2B5C32C500877059 /* NotificationView.swift in Sources */, DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */, DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */, DB63F769279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift in Sources */, @@ -3866,6 +3881,7 @@ DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */, DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */, D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */, + D80F627D2B5C32C500877059 /* NotificationView+ViewModel.swift in Sources */, DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */, DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */, DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */, diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift similarity index 100% rename from Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift rename to Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift similarity index 90% rename from MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift rename to Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift index e0eb3e312..6bec29e20 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift @@ -15,6 +15,7 @@ import MastodonExtension import MastodonCore import CoreData import CoreDataStack +import MastodonUI extension NotificationView { public final class ViewModel: ObservableObject { @@ -201,35 +202,15 @@ extension NotificationView.ViewModel { $authorName, $isMuting, $isBlocking, - Publishers.CombineLatest3( - $isMyself, - $isTranslated, - $isFollowed - ) + $isMyself ) - .sink { [weak self] authorName, isMuting, isBlocking, isMyselfIsTranslatedIsFollowed in - guard let name = authorName?.string, let self, let context = self.context, let authContext = self.authContext else { + .sink { [weak self] authorName, isMuting, isBlocking, isMyself in + guard let name = authorName?.string else { notificationView.menuButton.menu = nil return } - let (isMyself, isTranslated, isFollowed) = isMyselfIsTranslatedIsFollowed - - let authentication = authContext.mastodonAuthenticationBox.authentication - let instance = authentication.instance(in: context.managedObjectContext) - let isTranslationEnabled = instance?.isTranslationEnabled ?? false - - let menuContext = NotificationView.AuthorMenuContext( - name: name, - isMuting: isMuting, - isBlocking: isBlocking, - isMyself: isMyself, - isBookmarking: false, // no bookmark action display for notification item - isFollowed: isFollowed, - isTranslationEnabled: isTranslationEnabled, - isTranslated: isTranslated, - statusLanguage: nil - ) + let menuContext = NotificationView.AuthorMenuContext(name: name, isMuting: isMuting, isBlocking: isBlocking, isMyself: isMyself) let (menu, actions) = notificationView.setupAuthorMenu(menuContext: menuContext) notificationView.menuButton.menu = menu notificationView.authorActions = actions diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift similarity index 99% rename from MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift rename to Mastodon/Scene/Notification/NotificationView/NotificationView.swift index da257f0c0..548ab0f04 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift @@ -12,6 +12,7 @@ import Meta import MastodonCore import MastodonAsset import MastodonLocalization +import MastodonUI public protocol NotificationViewDelegate: AnyObject { func notificationView(_ notificationView: NotificationView, authorAvatarButtonDidPressed button: AvatarButton) @@ -476,9 +477,14 @@ extension NotificationView: AdaptiveContainerView { } extension NotificationView { - @available(*, deprecated, message: "Does way too much") - public typealias AuthorMenuContext = StatusAuthorView.AuthorMenuContext + public struct AuthorMenuContext { + public let name: String + public let isMuting: Bool + public let isBlocking: Bool + public let isMyself: Bool + } + public func setupAuthorMenu(menuContext: AuthorMenuContext) -> (UIMenu, [UIAccessibilityCustomAction]) { var actions: [[MastodonMenu.Action]] = [] var upperActions: [MastodonMenu.Action] = [] From 018cf540845f0373c7eab3833f2b90a5e6e31aae Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 20 Jan 2024 20:03:45 +0100 Subject: [PATCH 125/159] Don't use publisher for most properties in NotificationView (IOS-192) Timestamp and A11y are still missing (and A11y is broken atm) --- .../Notification/NotificationSection.swift | 1 - .../NotificationView+Configuration.swift | 74 ++++++++++------ .../NotificationView+ViewModel.swift | 84 +------------------ .../NotificationView/NotificationView.swift | 2 +- 4 files changed, 52 insertions(+), 109 deletions(-) diff --git a/Mastodon/Diffable/Notification/NotificationSection.swift b/Mastodon/Diffable/Notification/NotificationSection.swift index 0b446336f..e95c681db 100644 --- a/Mastodon/Diffable/Notification/NotificationSection.swift +++ b/Mastodon/Diffable/Notification/NotificationSection.swift @@ -73,7 +73,6 @@ extension NotificationSection { viewModel: NotificationTableViewCell.ViewModel, configuration: Configuration ) { - cell.notificationView.viewModel.context = context cell.notificationView.viewModel.authContext = configuration.authContext StatusSection.setupStatusPollDataSource( diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index 1f77a2d33..0cb9da883 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -64,23 +64,32 @@ extension NotificationView { let author = notification.account // author avatar - viewModel.authorAvatarImageURL = author.avatarImageURL() + let configuration = AvatarImageView.Configuration(url: author.avatarImageURL()) + avatarButton.avatarImageView.configure(configuration: configuration) + avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) // author name + let metaAuthorName: MetaContent do { let content = MastodonContent(content: author.displayNameWithFallback, emojis: author.emojis.asDictionary) - viewModel.authorName = try MastodonMetaContent.convert(document: content) + metaAuthorName = try MastodonMetaContent.convert(document: content) } catch { assertionFailure(error.localizedDescription) - viewModel.authorName = PlaintextMetaContent(string: author.displayNameWithFallback) + metaAuthorName = PlaintextMetaContent(string: author.displayNameWithFallback) } + authorNameLabel.configure(content: metaAuthorName) + + // username + let metaUsername = PlaintextMetaContent(string: "@\(author.acct)") + authorUsernameLabel.configure(content: metaUsername) - viewModel.authorUsername = author.acct viewModel.timestamp = notification.entity.createdAt - viewModel.visibility = notification.entity.status?.mastodonVisibility ?? ._other("") + let visibility = notification.entity.status?.mastodonVisibility ?? ._other("") + visibilityIconImageView.image = visibility.image // notification type indicator + let notificationIndicatorText: MetaContent? if let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) { self.viewModel.type = type @@ -92,64 +101,81 @@ extension NotificationView { } return metaContent } - + switch type { case .follow: - self.viewModel.notificationIndicatorText = createMetaContent( + notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.followedYou, emojis: author.emojis.asDictionary ) case .followRequest: - self.viewModel.notificationIndicatorText = createMetaContent( + notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou, emojis: author.emojis.asDictionary ) case .mention: - self.viewModel.notificationIndicatorText = createMetaContent( + notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.mentionedYou, emojis: author.emojis.asDictionary ) case .reblog: - self.viewModel.notificationIndicatorText = createMetaContent( + notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost, emojis: author.emojis.asDictionary ) case .favourite: - self.viewModel.notificationIndicatorText = createMetaContent( + notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost, emojis: author.emojis.asDictionary ) case .poll: - self.viewModel.notificationIndicatorText = createMetaContent( + notificationIndicatorText = createMetaContent( text: L10n.Scene.Notification.NotificationDescription.pollHasEnded, emojis: author.emojis.asDictionary ) case .status: - self.viewModel.notificationIndicatorText = createMetaContent( + notificationIndicatorText = createMetaContent( text: .empty, emojis: author.emojis.asDictionary ) case ._other: - self.viewModel.notificationIndicatorText = nil + notificationIndicatorText = nil } } else { - self.viewModel.notificationIndicatorText = nil + notificationIndicatorText = nil } - + + if let notificationIndicatorText { + notificationTypeIndicatorLabel.configure(content: notificationIndicatorText) + } else { + notificationTypeIndicatorLabel.reset() + } + if let me = viewModel.authContext?.mastodonAuthenticationBox.authentication.account() { - viewModel.isMyself = (author == me) - + let isMyself = (author == me) + let isMuting: Bool + let isBlocking: Bool + if let relationship = notification.relationship { - viewModel.isMuting = relationship.muting - viewModel.isBlocking = relationship.blocking || relationship.domainBlocking - viewModel.isFollowed = relationship.following + isMuting = relationship.muting + isBlocking = relationship.blocking || relationship.domainBlocking } else { - viewModel.isMuting = false - viewModel.isBlocking = false - viewModel.isFollowed = false + isMuting = false + isBlocking = false } + + let menuContext = NotificationView.AuthorMenuContext(name: metaAuthorName.string, isMuting: isMuting, isBlocking: isBlocking, isMyself: isMyself) + let (menu, actions) = setupAuthorMenu(menuContext: menuContext) + menuButton.menu = menu + authorActions = actions + menuButton.showsMenuAsPrimaryAction = true + + menuButton.isHidden = menuContext.isMyself + } + + viewModel.followRequestState = notification.followRequestState viewModel.transientFollowRequestState = notification.transientFollowRequestState } diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift index 6bec29e20..0117395a5 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift @@ -21,25 +21,15 @@ extension NotificationView { public final class ViewModel: ObservableObject { public var disposeBag = Set() - @Published public var context: AppContext? @Published public var authContext: AuthContext? @Published public var type: MastodonNotificationType? @Published public var notificationIndicatorText: MetaContent? - @Published public var authorAvatarImageURL: URL? @Published public var authorName: MetaContent? @Published public var authorUsername: String? - - @Published public var isMyself = false - @Published public var isMuting = false - @Published public var isBlocking = false - @Published public var isTranslated = false - @Published public var isFollowed = false - + @Published public var timestamp: Date? - - @Published public var visibility: MastodonVisibility = .public @Published public var followRequestState = MastodonFollowRequestState(state: .none) @Published public var transientFollowRequestState = MastodonFollowRequestState(state: .none) @@ -54,12 +44,8 @@ extension NotificationView { extension NotificationView.ViewModel { func bind(notificationView: NotificationView) { bindAuthor(notificationView: notificationView) - bindAuthorMenu(notificationView: notificationView) bindFollowRequest(notificationView: notificationView) - $context - .assign(to: \.context, on: notificationView.statusView.viewModel) - .store(in: &disposeBag) $authContext .assign(to: \.authContext, on: notificationView.statusView.viewModel) .store(in: &disposeBag) @@ -69,33 +55,6 @@ extension NotificationView.ViewModel { } private func bindAuthor(notificationView: NotificationView) { - // avatar - - $authorAvatarImageURL - .sink { url in - let configuration = AvatarImageView.Configuration(url: url) - notificationView.avatarButton.avatarImageView.configure(configuration: configuration) - notificationView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12))) - } - .store(in: &disposeBag) - // name - $authorName - .sink { metaContent in - let metaContent = metaContent ?? PlaintextMetaContent(string: " ") - notificationView.authorNameLabel.configure(content: metaContent) - } - .store(in: &disposeBag) - // username - $authorUsername - .map { text -> String in - guard let text = text else { return "" } - return "@\(text)" - } - .sink { username in - let metaContent = PlaintextMetaContent(string: username) - notificationView.authorUsernameLabel.configure(content: metaContent) - } - .store(in: &disposeBag) // timestamp let formattedTimestamp = Publishers.CombineLatest( $timestamp, @@ -112,23 +71,6 @@ extension NotificationView.ViewModel { } .store(in: &disposeBag) - $visibility - .sink { visibility in - notificationView.visibilityIconImageView.image = visibility.image - } - .store(in: &disposeBag) - - // notification type indicator - $notificationIndicatorText - .sink { text in - if let text = text { - notificationView.notificationTypeIndicatorLabel.configure(content: text) - } else { - notificationView.notificationTypeIndicatorLabel.reset() - } - } - .store(in: &disposeBag) - Publishers.CombineLatest4( $authorName, $authorUsername, @@ -197,30 +139,6 @@ extension NotificationView.ViewModel { .store(in: &disposeBag) } - private func bindAuthorMenu(notificationView: NotificationView) { - Publishers.CombineLatest4( - $authorName, - $isMuting, - $isBlocking, - $isMyself - ) - .sink { [weak self] authorName, isMuting, isBlocking, isMyself in - guard let name = authorName?.string else { - notificationView.menuButton.menu = nil - return - } - - let menuContext = NotificationView.AuthorMenuContext(name: name, isMuting: isMuting, isBlocking: isBlocking, isMyself: isMyself) - let (menu, actions) = notificationView.setupAuthorMenu(menuContext: menuContext) - notificationView.menuButton.menu = menu - notificationView.authorActions = actions - notificationView.menuButton.showsMenuAsPrimaryAction = true - - notificationView.menuButton.isHidden = menuContext.isMyself - } - .store(in: &disposeBag) - } - private func bindFollowRequest(notificationView: NotificationView) { Publishers.CombineLatest( $followRequestState, diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift index 548ab0f04..3ba5c2bd1 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift @@ -181,7 +181,7 @@ public final class NotificationView: UIView { disposeBag.removeAll() viewModel.authContext = nil - viewModel.authorAvatarImageURL = nil + avatarButton.avatarImageView.image = nil avatarButton.avatarImageView.cancelTask() authorContainerViewBottomPaddingView.isHidden = true From e720db2a31ae329fcb4d561e32ab29c8db5d16e3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 20 Jan 2024 20:03:59 +0100 Subject: [PATCH 126/159] Remove unused code (IOS-192) --- .../Persistence/Persistence+MastodonUser.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift index ae6ea3f0e..16615f7e0 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift @@ -110,14 +110,6 @@ extension Persistence.MastodonUser { ) user.update(property: property) } - - private static func update( - mastodonUser user: MastodonUser, - context: PersistContext - ) { - // TODO: - } // end func update - } extension Persistence.MastodonUser { From 304eb8b7ff156b6326843b91786961d3fa240f8f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Sat, 20 Jan 2024 21:51:04 +0100 Subject: [PATCH 127/159] Move notification-timestamp away from viewmodel (IOS-192) --- .../NotificationView+Configuration.swift | 14 +++++--- .../NotificationView+ViewModel.swift | 36 +++++-------------- .../NotificationView/NotificationView.swift | 7 +++- .../Sources/MastodonUI/Extension/Date.swift | 4 +-- 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index 0cb9da883..e1c4b6d6d 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -83,8 +83,6 @@ extension NotificationView { let metaUsername = PlaintextMetaContent(string: "@\(author.acct)") authorUsernameLabel.configure(content: metaUsername) - viewModel.timestamp = notification.entity.createdAt - let visibility = notification.entity.status?.mastodonVisibility ?? ._other("") visibilityIconImageView.image = visibility.image @@ -174,9 +172,15 @@ extension NotificationView { } + timestampUpdatePublisher + .prepend(Date()) + .eraseToAnyPublisher() + .sink { [weak self] now in + guard let self else { return } - - viewModel.followRequestState = notification.followRequestState - viewModel.transientFollowRequestState = notification.transientFollowRequestState + let timestamp = now.localizedTimeAgo(since: notification.entity.createdAt) + dateLabel.configure(content: PlaintextMetaContent(string: timestamp)) + } + .store(in: &disposeBag) } } diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift index 0117395a5..7c64d217e 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift @@ -33,19 +33,11 @@ extension NotificationView { @Published public var followRequestState = MastodonFollowRequestState(state: .none) @Published public var transientFollowRequestState = MastodonFollowRequestState(state: .none) - - let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) - .autoconnect() - .share() - .eraseToAnyPublisher() } } extension NotificationView.ViewModel { func bind(notificationView: NotificationView) { - bindAuthor(notificationView: notificationView) - bindFollowRequest(notificationView: notificationView) - $authContext .assign(to: \.authContext, on: notificationView.statusView.viewModel) .store(in: &disposeBag) @@ -56,32 +48,19 @@ extension NotificationView.ViewModel { private func bindAuthor(notificationView: NotificationView) { // timestamp - let formattedTimestamp = Publishers.CombineLatest( - $timestamp, - timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() - ) - .map { timestamp, _ in - timestamp?.localizedTimeAgoSinceNow ?? "" - } - .removeDuplicates() - - formattedTimestamp - .sink { timestamp in - notificationView.dateLabel.configure(content: PlaintextMetaContent(string: timestamp)) - } - .store(in: &disposeBag) - Publishers.CombineLatest4( $authorName, $authorUsername, $notificationIndicatorText, - formattedTimestamp + $timestamp ) .sink { name, username, type, timestamp in + + let formattedTimestamp = timestamp?.localizedSlowedTimeAgoSinceNow ?? "" notificationView.accessibilityLabel = [ "\(name?.string ?? "") \(type?.string ?? "")", username.map { "@\($0)" } ?? "", - timestamp + formattedTimestamp ].joined(separator: ", ") if !notificationView.statusView.isHidden { notificationView.accessibilityLabel! += ", " + (notificationView.statusView.accessibilityLabel ?? "") @@ -138,7 +117,7 @@ extension NotificationView.ViewModel { } .store(in: &disposeBag) } - + private func bindFollowRequest(notificationView: NotificationView) { Publishers.CombineLatest( $followRequestState, @@ -159,7 +138,7 @@ extension NotificationView.ViewModel { default: break } - + let state = transientFollowRequestState.state if state == .isAccepting { notificationView.acceptFollowRequestActivityIndicatorView.startAnimating() @@ -179,7 +158,7 @@ extension NotificationView.ViewModel { notificationView.rejectFollowRequestButton.tintColor = .black notificationView.rejectFollowRequestButton.setTitleColor(.black, for: .normal) } - + UIView.animate(withDuration: 0.3) { if state == .isAccept { notificationView.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true @@ -193,3 +172,4 @@ extension NotificationView.ViewModel { } } + diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift index 3ba5c2bd1..349fc80b0 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift @@ -176,7 +176,12 @@ public final class NotificationView: UIView { public let quoteStatusViewContainerView = UIView() public let quoteBackgroundView = UIView() public let quoteStatusView = StatusView() - + + let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() + public func prepareForReuse() { disposeBag.removeAll() diff --git a/MastodonSDK/Sources/MastodonUI/Extension/Date.swift b/MastodonSDK/Sources/MastodonUI/Extension/Date.swift index 2b0cb9098..771d4c11a 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/Date.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/Date.swift @@ -32,10 +32,10 @@ extension Date { } public var localizedTimeAgoSinceNow: String { - return self.localizedTimeAgo(since: Date(), isSlowed: false, isAbbreviated: false) + return self.localizedTimeAgo(since: Date()) } - public func localizedTimeAgo(since date: Date, isSlowed: Bool, isAbbreviated: Bool) -> String { + public func localizedTimeAgo(since date: Date, isSlowed: Bool = false, isAbbreviated: Bool = false) -> String { let earlierDate = date < self ? date : self let latestDate = earlierDate == date ? self : date From 96c4fbfbdb3fadb1144145591599c168d77c063b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 24 Jan 2024 11:58:08 +0100 Subject: [PATCH 128/159] Set a11y-label for notification-screen (IOS-192) --- .../NotificationView+Configuration.swift | 19 +++++++++++--- .../NotificationView+ViewModel.swift | 26 +------------------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index e1c4b6d6d..afa4dda20 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -176,10 +176,23 @@ extension NotificationView { .prepend(Date()) .eraseToAnyPublisher() .sink { [weak self] now in - guard let self else { return } + guard let self, let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) else { return } + + let formattedTimestamp = now.localizedTimeAgo(since: notification.entity.createdAt) + dateLabel.configure(content: PlaintextMetaContent(string: formattedTimestamp)) + + self.accessibilityLabel = [ + "\(author.displayNameWithFallback) \(type)", + author.acct, + formattedTimestamp + ].joined(separator: ", ") + if self.statusView.isHidden == false { + self.accessibilityLabel! += ", " + (self.statusView.accessibilityLabel ?? "") + } + if self.quoteStatusViewContainerView.isHidden == false { + self.accessibilityLabel! += ", " + (self.quoteStatusView.accessibilityLabel ?? "") + } - let timestamp = now.localizedTimeAgo(since: notification.entity.createdAt) - dateLabel.configure(content: PlaintextMetaContent(string: timestamp)) } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift index 7c64d217e..f4c3379e1 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift @@ -47,31 +47,7 @@ extension NotificationView.ViewModel { } private func bindAuthor(notificationView: NotificationView) { - // timestamp - Publishers.CombineLatest4( - $authorName, - $authorUsername, - $notificationIndicatorText, - $timestamp - ) - .sink { name, username, type, timestamp in - - let formattedTimestamp = timestamp?.localizedSlowedTimeAgoSinceNow ?? "" - notificationView.accessibilityLabel = [ - "\(name?.string ?? "") \(type?.string ?? "")", - username.map { "@\($0)" } ?? "", - formattedTimestamp - ].joined(separator: ", ") - if !notificationView.statusView.isHidden { - notificationView.accessibilityLabel! += ", " + (notificationView.statusView.accessibilityLabel ?? "") - } - if !notificationView.quoteStatusViewContainerView.isHidden { - notificationView.accessibilityLabel! += ", " + (notificationView.quoteStatusView.accessibilityLabel ?? "") - } - } - .store(in: &disposeBag) - - $type + $type .sink { type in var actions = [UIAccessibilityCustomAction]() From f3633166625e4d2163622261042895919356a9b5 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 24 Jan 2024 14:16:04 +0100 Subject: [PATCH 129/159] Move over a11y-actions for notifications (IOS-192) --- .../NotificationView+Configuration.swift | 44 +++++++++++++++++ .../NotificationView+ViewModel.swift | 48 ------------------- 2 files changed, 44 insertions(+), 48 deletions(-) diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index afa4dda20..c8b14db01 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -139,8 +139,52 @@ extension NotificationView { case ._other: notificationIndicatorText = nil } + + var actions = [UIAccessibilityCustomAction]() + + // these notifications can be directly actioned to view the profile + if type != .follow, type != .followRequest { + actions.append( + UIAccessibilityCustomAction( + name: L10n.Common.Controls.Status.showUserProfile, + image: nil + ) { [weak self] _ in + guard let self, let delegate = self.delegate else { return false } + delegate.notificationView(self, authorAvatarButtonDidPressed: self.avatarButton) + return true + } + ) + } + + if type == .followRequest { + actions.append( + UIAccessibilityCustomAction( + name: L10n.Common.Controls.Actions.confirm, + image: Asset.Editing.checkmark20.image + ) { [weak self] _ in + guard let self, let delegate = self.delegate else { return false } + delegate.notificationView(self, acceptFollowRequestButtonDidPressed: self.acceptFollowRequestButton) + return true + } + ) + + actions.append( + UIAccessibilityCustomAction( + name: L10n.Common.Controls.Actions.delete, + image: Asset.Circles.forbidden20.image + ) { [weak self] _ in + guard let self, let delegate = self.delegate else { return false } + delegate.notificationView(self, rejectFollowRequestButtonDidPressed: self.rejectFollowRequestButton) + return true + } + ) + } + + notificationActions = actions + } else { notificationIndicatorText = nil + notificationActions = [] } if let notificationIndicatorText { diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift index f4c3379e1..f463d6e84 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift @@ -45,54 +45,6 @@ extension NotificationView.ViewModel { .assign(to: \.authContext, on: notificationView.quoteStatusView.viewModel) .store(in: &disposeBag) } - - private func bindAuthor(notificationView: NotificationView) { - $type - .sink { type in - var actions = [UIAccessibilityCustomAction]() - - // these notifications can be directly actioned to view the profile - if type != .follow, type != .followRequest { - actions.append( - UIAccessibilityCustomAction( - name: L10n.Common.Controls.Status.showUserProfile, - image: nil - ) { [weak notificationView] _ in - guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false } - delegate.notificationView(notificationView, authorAvatarButtonDidPressed: notificationView.avatarButton) - return true - } - ) - } - - if type == .followRequest { - actions.append( - UIAccessibilityCustomAction( - name: L10n.Common.Controls.Actions.confirm, - image: Asset.Editing.checkmark20.image - ) { [weak notificationView] _ in - guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false } - delegate.notificationView(notificationView, acceptFollowRequestButtonDidPressed: notificationView.acceptFollowRequestButton) - return true - } - ) - - actions.append( - UIAccessibilityCustomAction( - name: L10n.Common.Controls.Actions.delete, - image: Asset.Circles.forbidden20.image - ) { [weak notificationView] _ in - guard let notificationView = notificationView, let delegate = notificationView.delegate else { return false } - delegate.notificationView(notificationView, rejectFollowRequestButtonDidPressed: notificationView.rejectFollowRequestButton) - return true - } - ) - } - - notificationView.notificationActions = actions - } - .store(in: &disposeBag) - } private func bindFollowRequest(notificationView: NotificationView) { Publishers.CombineLatest( From 7dfa56507e5e2accd7ce8714e748bd159732e7e9 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 30 Jan 2024 11:13:39 +0100 Subject: [PATCH 130/159] Make follow-requests work again (IOS-192) Aaaaand remove obsolete viewmodel and put relationship to make menu work again (too) --- Mastodon.xcodeproj/project.pbxproj | 4 - .../Notification/NotificationSection.swift | 5 +- .../Provider/DataSourceFacade+Follow.swift | 50 ++++----- ...er+NotificationTableViewCellDelegate.swift | 14 ++- .../NotificationTableViewCell+ViewModel.swift | 6 +- ...ineViewController+DataSourceProvider.swift | 8 +- .../NotificationTimelineViewController.swift | 2 - .../NotificationView+Configuration.swift | 66 ++++++++--- .../NotificationView+ViewModel.swift | 103 ------------------ .../NotificationView/NotificationView.swift | 7 -- .../MastodonSDK/MastodonNotification.swift | 4 +- 11 files changed, 94 insertions(+), 175 deletions(-) delete mode 100644 Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e713b0afe..4b20158d7 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -133,7 +133,6 @@ D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8099079294BC9390050219F /* PrivacyTableViewCell.swift */; }; D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D809907B294D25510050219F /* PrivacyViewModel.swift */; }; D80F627C2B5C32C500877059 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80F627A2B5C32C500877059 /* NotificationView.swift */; }; - D80F627D2B5C32C500877059 /* NotificationView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80F627B2B5C32C500877059 /* NotificationView+ViewModel.swift */; }; D81439862AD415DE0071A88F /* AboutInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81439852AD415DE0071A88F /* AboutInstance.swift */; }; D81439882AD450A40071A88F /* AboutInstanceTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81439872AD450A40071A88F /* AboutInstanceTableViewDataSource.swift */; }; D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */; }; @@ -787,7 +786,6 @@ D8099079294BC9390050219F /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = ""; }; D809907B294D25510050219F /* PrivacyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyViewModel.swift; sourceTree = ""; }; D80F627A2B5C32C500877059 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; - D80F627B2B5C32C500877059 /* NotificationView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationView+ViewModel.swift"; sourceTree = ""; }; D81439852AD415DE0071A88F /* AboutInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstance.swift; sourceTree = ""; }; D81439872AD450A40071A88F /* AboutInstanceTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstanceTableViewDataSource.swift; sourceTree = ""; }; D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverviewTableViewController.swift; sourceTree = ""; }; @@ -1824,7 +1822,6 @@ isa = PBXGroup; children = ( D80F627A2B5C32C500877059 /* NotificationView.swift */, - D80F627B2B5C32C500877059 /* NotificationView+ViewModel.swift */, DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */, ); path = NotificationView; @@ -3881,7 +3878,6 @@ DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */, DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */, D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */, - D80F627D2B5C32C500877059 /* NotificationView+ViewModel.swift in Sources */, DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */, DBEFCD71282A12B200C0ABEA /* ReportReasonViewController.swift in Sources */, DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */, diff --git a/Mastodon/Diffable/Notification/NotificationSection.swift b/Mastodon/Diffable/Notification/NotificationSection.swift index e95c681db..7965ddc0a 100644 --- a/Mastodon/Diffable/Notification/NotificationSection.swift +++ b/Mastodon/Diffable/Notification/NotificationSection.swift @@ -73,8 +73,6 @@ extension NotificationSection { viewModel: NotificationTableViewCell.ViewModel, configuration: Configuration ) { - cell.notificationView.viewModel.authContext = configuration.authContext - StatusSection.setupStatusPollDataSource( context: context, authContext: configuration.authContext, @@ -90,7 +88,8 @@ extension NotificationSection { cell.configure( tableView: tableView, viewModel: viewModel, - delegate: configuration.notificationTableViewCellDelegate + delegate: configuration.notificationTableViewCellDelegate, + authenticationBox: configuration.authContext.mastodonAuthenticationBox ) cell.notificationView.statusView.viewModel.filterContext = configuration.filterContext diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 9037fde33..249ff5358 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -35,44 +35,47 @@ extension DataSourceFacade { static func responseToUserFollowRequestAction( dependency: NeedsDependency & AuthContextProvider, notification: MastodonNotification, + notificationView: NotificationView, query: Mastodon.API.Account.FollowRequestQuery ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() - - let managedObjectContext = dependency.context.managedObjectContext - let _userID: String? = try await managedObjectContext.perform { - return notification.account.id - } - - guard let userID = _userID else { - assertionFailure() - throw APIService.APIError.implicit(.badRequest) - } - + + let userID = notification.account.id let state: MastodonFollowRequestState = notification.followRequestState - guard state.state == .none else { - return - } - + guard state.state == .none else { return } + switch query { case .accept: notification.transientFollowRequestState = .init(state: .isAccepting) case .reject: notification.transientFollowRequestState = .init(state: .isRejecting) } - + + await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox) + do { - _ = try await dependency.context.apiService.followRequest( + let newRelationship = try await dependency.context.apiService.followRequest( userID: userID, query: query, authenticationBox: dependency.authContext.mastodonAuthenticationBox - ) + ).value + + switch query { + case .accept: + notification.transientFollowRequestState = .init(state: .isAccept) + notification.followRequestState = .init(state: .isAccept) + case .reject: + break + } + + await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox) } catch { // reset state when failure notification.transientFollowRequestState = .init(state: .none) - + await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox) + if let error = error as? Mastodon.API.Error { switch error.httpResponseStatus { case .notFound: @@ -88,17 +91,8 @@ extension DataSourceFacade { ) } } - - return } - switch query { - case .accept: - notification.transientFollowRequestState = .init(state: .isAccept) - notification.followRequestState = .init(state: .isAccept) - case .reject: - break - } } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 7d41d27ff..6a56fca29 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -44,10 +44,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut barButtonItem: nil ), completion: { (newRelationship: Mastodon.Entity.Relationship) in - //TODO: Update Relationship. - //TODO: Get Relationship into here, first! - - print(newRelationship) + notification.relationship = newRelationship + Task { @MainActor in + notificationView.configure(notification: notification, authenticationBox: self.authContext.mastodonAuthenticationBox) + } } ) case .reportUser(_): @@ -117,9 +117,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut try await DataSourceFacade.responseToUserFollowRequestAction( dependency: self, notification: notification, + notificationView: notificationView, query: .accept ) - } // end Task + } } func tableViewCell( @@ -141,9 +142,10 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut try await DataSourceFacade.responseToUserFollowRequestAction( dependency: self, notification: notification, + notificationView: notificationView, query: .reject ) - } // end Task + } } } diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift index c45d722e4..9e0c4722a 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift @@ -9,6 +9,7 @@ import UIKit import Combine import CoreDataStack import MastodonSDK +import MastodonCore extension NotificationTableViewCell { final class ViewModel { @@ -29,7 +30,8 @@ extension NotificationTableViewCell { func configure( tableView: UITableView, viewModel: ViewModel, - delegate: NotificationTableViewCellDelegate? + delegate: NotificationTableViewCellDelegate?, + authenticationBox: MastodonAuthenticationBox ) { if notificationView.frame == .zero { // set status view width @@ -41,7 +43,7 @@ extension NotificationTableViewCell { switch viewModel.value { case .feed(let feed): - notificationView.configure(feed: feed) + notificationView.configure(feed: feed, authenticationBox: authenticationBox) } self.delegate = delegate diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index f23308cb4..42061d9bc 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -24,10 +24,10 @@ extension NotificationTimelineViewController: DataSourceProvider { case .feed(let feed): let item: DataSourceItem? = { guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil } - + //TODO: Get relationship - if let notification = feed.notification, - let mastodonNotification = MastodonNotification.fromEntity(notification, relationship: nil, domain: authContext.mastodonAuthenticationBox.domain) { + if let notification = feed.notification { + let mastodonNotification = MastodonNotification.fromEntity(notification, relationship: nil) return .notification(record: mastodonNotification) } else { return nil @@ -38,7 +38,7 @@ extension NotificationTimelineViewController: DataSourceProvider { return nil } } - + func update(status: MastodonStatus) { viewModel.dataController.update(status: status) } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 8c88afde2..e5c29cbd6 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -38,8 +38,6 @@ final class NotificationTimelineViewController: UIViewController, NeedsDependenc }() let cellFrameCache = NSCache() - - } extension NotificationTimelineViewController { diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index c8b14db01..26c6cc2e4 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -18,23 +18,24 @@ import MastodonLocalization import MastodonSDK extension NotificationView { - public func configure(feed: MastodonFeed) { + public func configure(feed: MastodonFeed, authenticationBox: MastodonAuthenticationBox) { guard let notification = feed.notification else { assertionFailure() return } - MastodonNotification.fromEntity( + let entity = MastodonNotification.fromEntity( notification, - relationship: feed.relationship, - domain: viewModel.authContext?.mastodonAuthenticationBox.domain ?? "" - ).map(configure(notification:)) + relationship: feed.relationship + ) + + configure(notification: entity, authenticationBox: authenticationBox) } } extension NotificationView { - public func configure(notification: MastodonNotification) { - configureAuthor(notification: notification) + public func configure(notification: MastodonNotification, authenticationBox: MastodonAuthenticationBox) { + configureAuthor(notification: notification, authenticationBox: authenticationBox) switch notification.entity.type { case .follow: @@ -57,10 +58,8 @@ extension NotificationView { } } -} -extension NotificationView { - private func configureAuthor(notification: MastodonNotification) { + private func configureAuthor(notification: MastodonNotification, authenticationBox: MastodonAuthenticationBox) { let author = notification.account // author avatar @@ -89,8 +88,6 @@ extension NotificationView { // notification type indicator let notificationIndicatorText: MetaContent? if let type = MastodonNotificationType(rawValue: notification.entity.type.rawValue) { - self.viewModel.type = type - // TODO: fix the i18n. The subject should assert place at the string beginning func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent { let content = MastodonContent(content: text, emojis: emojis) @@ -193,7 +190,7 @@ extension NotificationView { notificationTypeIndicatorLabel.reset() } - if let me = viewModel.authContext?.mastodonAuthenticationBox.authentication.account() { + if let me = authenticationBox.authentication.account() { let isMyself = (author == me) let isMuting: Bool let isBlocking: Bool @@ -213,7 +210,6 @@ extension NotificationView { menuButton.showsMenuAsPrimaryAction = true menuButton.isHidden = menuContext.isMyself - } timestampUpdatePublisher @@ -239,5 +235,47 @@ extension NotificationView { } .store(in: &disposeBag) + + switch notification.followRequestState.state { + case .isAccept: + self.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true + self.acceptFollowRequestButton.isUserInteractionEnabled = false + self.acceptFollowRequestButton.setImage(nil, for: .normal) + self.acceptFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.accepted, for: .normal) + case .isReject: + self.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true + self.rejectFollowRequestButton.isUserInteractionEnabled = false + self.rejectFollowRequestButton.setImage(nil, for: .normal) + self.rejectFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.rejected, for: .normal) + default: + break + } + + let state = notification.transientFollowRequestState.state + if state == .isAccepting { + self.acceptFollowRequestActivityIndicatorView.startAnimating() + self.acceptFollowRequestButton.tintColor = .clear + self.acceptFollowRequestButton.setTitleColor(.clear, for: .normal) + } else { + self.acceptFollowRequestActivityIndicatorView.stopAnimating() + self.acceptFollowRequestButton.tintColor = .white + self.acceptFollowRequestButton.setTitleColor(.white, for: .normal) + } + if state == .isRejecting { + self.rejectFollowRequestActivityIndicatorView.startAnimating() + self.rejectFollowRequestButton.tintColor = .clear + self.rejectFollowRequestButton.setTitleColor(.clear, for: .normal) + } else { + self.rejectFollowRequestActivityIndicatorView.stopAnimating() + self.rejectFollowRequestButton.tintColor = .black + self.rejectFollowRequestButton.setTitleColor(.black, for: .normal) + } + + if state == .isAccept { + self.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true + } + if state == .isReject { + self.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true + } } } diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift deleted file mode 100644 index f463d6e84..000000000 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+ViewModel.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// NotificationView+ViewModel.swift -// -// -// Created by MainasuK on 2022-1-21. -// - -import UIKit -import Combine -import Meta -import MastodonSDK -import MastodonAsset -import MastodonLocalization -import MastodonExtension -import MastodonCore -import CoreData -import CoreDataStack -import MastodonUI - -extension NotificationView { - public final class ViewModel: ObservableObject { - public var disposeBag = Set() - - @Published public var authContext: AuthContext? - - @Published public var type: MastodonNotificationType? - @Published public var notificationIndicatorText: MetaContent? - - @Published public var authorName: MetaContent? - @Published public var authorUsername: String? - - @Published public var timestamp: Date? - - @Published public var followRequestState = MastodonFollowRequestState(state: .none) - @Published public var transientFollowRequestState = MastodonFollowRequestState(state: .none) - } -} - -extension NotificationView.ViewModel { - func bind(notificationView: NotificationView) { - $authContext - .assign(to: \.authContext, on: notificationView.statusView.viewModel) - .store(in: &disposeBag) - $authContext - .assign(to: \.authContext, on: notificationView.quoteStatusView.viewModel) - .store(in: &disposeBag) - } - - private func bindFollowRequest(notificationView: NotificationView) { - Publishers.CombineLatest( - $followRequestState, - $transientFollowRequestState - ) - .sink { followRequestState, transientFollowRequestState in - switch followRequestState.state { - case .isAccept: - notificationView.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true - notificationView.acceptFollowRequestButton.isUserInteractionEnabled = false - notificationView.acceptFollowRequestButton.setImage(nil, for: .normal) - notificationView.acceptFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.accepted, for: .normal) - case .isReject: - notificationView.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true - notificationView.rejectFollowRequestButton.isUserInteractionEnabled = false - notificationView.rejectFollowRequestButton.setImage(nil, for: .normal) - notificationView.rejectFollowRequestButton.setTitle(L10n.Scene.Notification.FollowRequest.rejected, for: .normal) - default: - break - } - - let state = transientFollowRequestState.state - if state == .isAccepting { - notificationView.acceptFollowRequestActivityIndicatorView.startAnimating() - notificationView.acceptFollowRequestButton.tintColor = .clear - notificationView.acceptFollowRequestButton.setTitleColor(.clear, for: .normal) - } else { - notificationView.acceptFollowRequestActivityIndicatorView.stopAnimating() - notificationView.acceptFollowRequestButton.tintColor = .white - notificationView.acceptFollowRequestButton.setTitleColor(.white, for: .normal) - } - if state == .isRejecting { - notificationView.rejectFollowRequestActivityIndicatorView.startAnimating() - notificationView.rejectFollowRequestButton.tintColor = .clear - notificationView.rejectFollowRequestButton.setTitleColor(.clear, for: .normal) - } else { - notificationView.rejectFollowRequestActivityIndicatorView.stopAnimating() - notificationView.rejectFollowRequestButton.tintColor = .black - notificationView.rejectFollowRequestButton.setTitleColor(.black, for: .normal) - } - - UIView.animate(withDuration: 0.3) { - if state == .isAccept { - notificationView.rejectFollowRequestButtonShadowBackgroundContainer.isHidden = true - } - if state == .isReject { - notificationView.acceptFollowRequestButtonShadowBackgroundContainer.isHidden = true - } - } - } - .store(in: &disposeBag) - } - -} - diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift index 349fc80b0..a4ccb2dd3 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift @@ -48,12 +48,6 @@ public final class NotificationView: UIView { var notificationActions = [UIAccessibilityCustomAction]() var authorActions = [UIAccessibilityCustomAction]() - public private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(notificationView: self) - return viewModel - }() - let containerStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical @@ -185,7 +179,6 @@ public final class NotificationView: UIView { public func prepareForReuse() { disposeBag.removeAll() - viewModel.authContext = nil avatarButton.avatarImageView.image = nil avatarButton.avatarImageView.cancelTask() diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift index fcedbbbe2..a92a33a14 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonNotification.swift @@ -11,7 +11,7 @@ public final class MastodonNotification { } public let account: Mastodon.Entity.Account - public let relationship: Mastodon.Entity.Relationship? + public var relationship: Mastodon.Entity.Relationship? public let status: MastodonStatus? public let feeds: [MastodonFeed] @@ -28,7 +28,7 @@ public final class MastodonNotification { } public extension MastodonNotification { - static func fromEntity(_ entity: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?, domain: String) -> MastodonNotification? { + static func fromEntity(_ entity: Mastodon.Entity.Notification, relationship: Mastodon.Entity.Relationship?) -> MastodonNotification { return MastodonNotification(entity: entity, account: entity.account, relationship: relationship, status: entity.status.map(MastodonStatus.fromEntity), feeds: []) } } From 53973bb4d96a89018c92ba6cb8d46d4f28c57ec7 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 31 Jan 2024 22:49:56 +0100 Subject: [PATCH 131/159] Revert "Use `guard` (IOS-192)" This reverts commit 64dc97ab9bb50041146777248885a67dc1fcf73b. --- Mastodon/Scene/Profile/ProfileViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index baa846bce..fdca816ec 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -221,7 +221,7 @@ extension ProfileViewController { } } - guard self.viewModel.account.suspended == false else { + if let suspended = self.viewModel.account.suspended, suspended == true { return } From f72de977de89088267f5b26d52d8d915489b1f3b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 31 Jan 2024 23:25:57 +0100 Subject: [PATCH 132/159] Enable domain-block for User-refactoring (IOS-192) --- Mastodon/Scene/Profile/ProfileViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index fdca816ec..051a6d6b3 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -421,13 +421,14 @@ extension ProfileViewController { viewModel.$relationship ) .asyncMap { [weak self] user, relationship -> UIMenu? in - guard let self, let relationship else { return nil } + guard let self, let relationship, let domain = user.domainFromAcct else { return nil } let name = user.displayNameWithFallback var menuActions: [MastodonMenu.Action] = [ .muteUser(.init(name: name, isMuting: relationship.muting)), .blockUser(.init(name: name, isBlocking: relationship.blocking)), + .blockDomain(.init(domain: domain, isBlocking: relationship.domainBlocking)), .reportUser(.init(name: name)), .shareUser(.init(name: name)), ] From 255b6ab0ef1034c187cfd7a1aa5f7885842e0fd3 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 9 Feb 2024 18:18:14 +0100 Subject: [PATCH 133/159] Remove obsolete code (IOS-192) We didn't use that as someone rebuilt this screen in SwiftUI. --- Mastodon.xcodeproj/project.pbxproj | 4 - Mastodon/Diffable/Report/ReportItem.swift | 1 - Mastodon/Diffable/Report/ReportSection.swift | 8 - .../ReportResultActionTableViewCell.swift | 140 ------------------ .../CoreDataStack/MastodonUser.swift | 102 ------------- 5 files changed, 255 deletions(-) delete mode 100644 Mastodon/Scene/Report/Share/Cell/ReportResultActionTableViewCell.swift delete mode 100644 MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 2693eb55a..4db188f6f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -407,7 +407,6 @@ DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */; }; DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */; }; DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; }; - DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; }; DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; }; DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; }; DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; }; @@ -1132,7 +1131,6 @@ DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCommentTableViewCell.swift; sourceTree = ""; }; DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewController.swift; sourceTree = ""; }; DB98EB6427B216500082E365 /* ReportResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewModel.swift; sourceTree = ""; }; - DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = ""; }; DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; @@ -2685,7 +2683,6 @@ DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */, DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */, DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */, - DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */, ); path = Cell; sourceTree = ""; @@ -3891,7 +3888,6 @@ 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, - DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */, DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, diff --git a/Mastodon/Diffable/Report/ReportItem.swift b/Mastodon/Diffable/Report/ReportItem.swift index ed083f427..cd5d9e9cf 100644 --- a/Mastodon/Diffable/Report/ReportItem.swift +++ b/Mastodon/Diffable/Report/ReportItem.swift @@ -13,7 +13,6 @@ enum ReportItem: Hashable { case header(context: HeaderContext) case status(record: MastodonStatus) case comment(context: CommentContext) - case result(record: ManagedObjectRecord) case bottomLoader } diff --git a/Mastodon/Diffable/Report/ReportSection.swift b/Mastodon/Diffable/Report/ReportSection.swift index 94161f28c..4461dc2f6 100644 --- a/Mastodon/Diffable/Report/ReportSection.swift +++ b/Mastodon/Diffable/Report/ReportSection.swift @@ -35,7 +35,6 @@ extension ReportSection { tableView.register(ReportHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: ReportHeadlineTableViewCell.self)) tableView.register(ReportStatusTableViewCell.self, forCellReuseIdentifier: String(describing: ReportStatusTableViewCell.self)) tableView.register(ReportCommentTableViewCell.self, forCellReuseIdentifier: String(describing: ReportCommentTableViewCell.self)) - tableView.register(ReportResultActionTableViewCell.self, forCellReuseIdentifier: String(describing: ReportResultActionTableViewCell.self)) tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in @@ -72,13 +71,6 @@ extension ReportSection { } .store(in: &cell.disposeBag) return cell - case .result(let record): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - cell.avatarImageView.configure(configuration: .init(url: user.avatarImageURL())) - } - return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell cell.activityIndicatorView.startAnimating() diff --git a/Mastodon/Scene/Report/Share/Cell/ReportResultActionTableViewCell.swift b/Mastodon/Scene/Report/Share/Cell/ReportResultActionTableViewCell.swift deleted file mode 100644 index 1828035a6..000000000 --- a/Mastodon/Scene/Report/Share/Cell/ReportResultActionTableViewCell.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// ReportResultActionTableViewCell.swift -// Mastodon -// -// Created by MainasuK on 2022-2-8. -// - -import UIKit -import Combine -import MastodonAsset -import MastodonUI -import MastodonLocalization - -final class ReportResultActionTableViewCell: UITableViewCell { - - var disposeBag = Set() - - let containerView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - return stackView - }() - - let avatarImageView: AvatarImageView = { - let imageView = AvatarImageView() - imageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 27))) - return imageView - }() - - let reportBannerShadowContainer = ShadowBackgroundContainer() - let reportBannerLabel: UILabel = { - let label = UILabel() - let padding = Array(repeating: " ", count: 2).joined() - label.text = padding + L10n.Scene.Report.reported + padding - label.textColor = Asset.Scene.Report.reportBanner.color - label.font = FontFamily.Staatliches.regular.font(size: 49) - label.backgroundColor = Asset.Scene.Report.background.color - label.layer.borderColor = Asset.Scene.Report.reportBanner.color.cgColor - label.layer.borderWidth = 6 - label.layer.masksToBounds = true - label.layer.cornerRadius = 12 - return label - }() - - override func prepareForReuse() { - super.prepareForReuse() - - disposeBag.removeAll() - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ReportResultActionTableViewCell { - - private func _init() { - selectionStyle = .none - backgroundColor = .clear - - containerView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(containerView) - NSLayoutConstraint.activate([ - containerView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), - containerView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), - containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - let avatarContainer = UIStackView() - avatarContainer.axis = .horizontal - containerView.addArrangedSubview(avatarContainer) - - let avatarLeadingPaddingView = UIView() - let avatarTrailingPaddingView = UIView() - avatarLeadingPaddingView.translatesAutoresizingMaskIntoConstraints = false - avatarContainer.addArrangedSubview(avatarLeadingPaddingView) - avatarImageView.translatesAutoresizingMaskIntoConstraints = false - avatarContainer.addArrangedSubview(avatarImageView) - avatarTrailingPaddingView.translatesAutoresizingMaskIntoConstraints = false - avatarContainer.addArrangedSubview(avatarTrailingPaddingView) - NSLayoutConstraint.activate([ - avatarImageView.widthAnchor.constraint(equalToConstant: 106).priority(.required - 1), - avatarImageView.heightAnchor.constraint(equalToConstant: 106).priority(.required - 1), - avatarLeadingPaddingView.widthAnchor.constraint(equalTo: avatarTrailingPaddingView.widthAnchor).priority(.defaultHigh), - ]) - - reportBannerShadowContainer.translatesAutoresizingMaskIntoConstraints = false - avatarContainer.addSubview(reportBannerShadowContainer) - NSLayoutConstraint.activate([ - reportBannerShadowContainer.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), - reportBannerShadowContainer.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), - ]) - reportBannerShadowContainer.transform = CGAffineTransform(rotationAngle: -(.pi / 180 * 5)) - - reportBannerLabel.translatesAutoresizingMaskIntoConstraints = false - reportBannerShadowContainer.addSubview(reportBannerLabel) - reportBannerLabel.pinToParent() - - } - - override func layoutSubviews() { - super.layoutSubviews() - - reportBannerShadowContainer.layer.setupShadow( - color: .black, - alpha: 0.25, - x: 1, - y: 0.64, - blur: 0.64, - spread: 0, - roundedRect: reportBannerShadowContainer.bounds, - byRoundingCorners: .allCorners, - cornerRadii: CGSize(width: 12, height: 12) - ) - } - -} - -#if DEBUG -import SwiftUI -struct ReportResultActionTableViewCell_Preview: PreviewProvider { - static var previews: some View { - UIViewPreview(width: 375) { - let cell = ReportResultActionTableViewCell() - cell.avatarImageView.configure(configuration: .init(image: .placeholder(color: .blue))) - return cell - } - .previewLayout(.fixed(width: 375, height: 106)) - } -} -#endif diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift deleted file mode 100644 index 6d952726c..000000000 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// MastodonUser.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/2/3. -// - -import Foundation -import CoreDataStack -import MastodonSDK -import MastodonMeta - -extension MastodonUser { - - public var displayNameWithFallback: String { - return !displayName.isEmpty ? displayName : username - } - - public var acctWithDomain: String { - if !acct.contains("@") { - // Safe concat due to username cannot contains "@" - return username + "@" + domain - } else { - return acct - } - } - - public var domainFromAcct: String { - if !acct.contains("@") { - return domain - } else { - let domain = acct.split(separator: "@").last - return String(domain!) - } - } - -} - -extension MastodonUser { - - public func headerImageURL() -> URL? { - return URL(string: header) - } - - public func headerImageURLWithFallback(domain: String) -> URL { - return URL(string: header) ?? URL(string: "https://\(domain)/headers/original/missing.png")! - } - - public func avatarImageURL() -> URL? { - let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar - return URL(string: string) - } - - public func avatarImageURLWithFallback(domain: String) -> URL { - return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! - } - -} - -extension MastodonUser { - - public var profileURL: URL { - if let urlString = self.url, - let url = URL(string: urlString) { - return url - } else { - return URL(string: "https://\(self.domain)/@\(username)")! - } - } - - public var activityItems: [Any] { - var items: [Any] = [] - items.append(profileURL) - return items - } - -} - -extension MastodonUser { - public var nameMetaContent: MastodonMetaContent? { - do { - let content = MastodonContent(content: displayNameWithFallback, emojis: emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure() - return nil - } - } - - public var bioMetaContent: MastodonMetaContent? { - guard let note = note else { return nil } - do { - let content = MastodonContent(content: note, emojis: emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure() - return nil - } - } -} From dc53fbbe7cf1406cff378cebaeec35e057a8c5db Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 9 Feb 2024 18:41:12 +0100 Subject: [PATCH 134/159] Remove more users (IOS-192) --- .../Entity/Mastodon/Status.swift | 62 ------------------- .../Persistence/Persistence+Status.swift | 17 ----- 2 files changed, 79 deletions(-) diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift index 8f6b95bf4..2cfa8c0f1 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift @@ -73,20 +73,10 @@ public final class Status: NSManagedObject { // many-to-one relationship // sourcery: autoGenerateRelationship - @NSManaged public private(set) var author: MastodonUser - // sourcery: autoGenerateRelationship @NSManaged public private(set) var reblog: Status? // sourcery: autoUpdatableObject @NSManaged public private(set) var replyTo: Status? - // many-to-many relationship - @NSManaged public private(set) var favouritedBy: Set - @NSManaged public private(set) var rebloggedBy: Set - @NSManaged public private(set) var mutedBy: Set - @NSManaged public private(set) var bookmarkedBy: Set - - // one-to-one relationship - @NSManaged public private(set) var pinnedBy: MastodonUser? // sourcery: autoGenerateRelationship @NSManaged public private(set) var poll: Poll? // sourcery: autoGenerateRelationship @@ -388,20 +378,17 @@ extension Status: AutoGenerateRelationship { // DO NOT EDIT public struct Relationship { public let application: Application? - public let author: MastodonUser public let reblog: Status? public let poll: Poll? public let card: Card? public init( application: Application?, - author: MastodonUser, reblog: Status?, poll: Poll?, card: Card? ) { self.application = application - self.author = author self.reblog = reblog self.poll = poll self.card = card @@ -410,7 +397,6 @@ extension Status: AutoGenerateRelationship { public func configure(relationship: Relationship) { self.application = relationship.application - self.author = relationship.author self.reblog = relationship.reblog self.poll = relationship.poll self.card = relationship.card @@ -536,54 +522,6 @@ extension Status: AutoUpdatableObject { } // sourcery:end - public func update(liked: Bool, by mastodonUser: MastodonUser) { - if liked { - if !self.favouritedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser) - } - } else { - if self.favouritedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser) - } - } - } - - public func update(reblogged: Bool, by mastodonUser: MastodonUser) { - if reblogged { - if !self.rebloggedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser) - } - } else { - if self.rebloggedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser) - } - } - } - - public func update(muted: Bool, by mastodonUser: MastodonUser) { - if muted { - if !self.mutedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser) - } - } else { - if self.mutedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser) - } - } - } - - public func update(bookmarked: Bool, by mastodonUser: MastodonUser) { - if bookmarked { - if !self.bookmarkedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser) - } - } else { - if self.bookmarkedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser) - } - } - } - public func update(isReveal: Bool) { revealedAt = isReveal ? Date() : nil } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift index c7548db82..9767b8b96 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift @@ -112,7 +112,6 @@ extension Persistence.Status { let relationship = Status.Relationship( application: application, - author: author, reblog: reblog, poll: poll, card: card @@ -170,7 +169,6 @@ extension Persistence.Status { property: property, relationship: relationship ) - update(status: status, context: context) return status } @@ -214,7 +212,6 @@ extension Persistence.Status { relationship: Status.Relationship( application: status.application, - author: status.author, reblog: status.reblog, poll: result.poll, card: status.card @@ -226,7 +223,6 @@ extension Persistence.Status { relationship: Status.Relationship( application: status.application, - author: status.author, reblog: status.reblog, poll: nil, card: status.card @@ -239,8 +235,6 @@ extension Persistence.Status { let relationship = Card.Relationship(status: status) card?.configure(relationship: relationship) } - - update(status: status, context: context) } private static func createCard( @@ -258,17 +252,6 @@ extension Persistence.Status { ) return result.card } - - private static func update( - status: Status, - context: PersistContext - ) { - // update friendships - if let user = context.me { - context.entity.reblogged.flatMap { status.update(reblogged: $0, by: user) } - context.entity.favourited.flatMap { status.update(liked: $0, by: user) } - } - } private static func createApplication( in managedObjectContext: NSManagedObjectContext, From 3ff509cb2767410defd0be0bf51d75d06e38d729 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 9 Feb 2024 19:10:24 +0100 Subject: [PATCH 135/159] Fix another user-warning (IOS-192) --- Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 06e1b2d8e..385ab8ef0 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -211,9 +211,8 @@ extension HomeTimelineViewController { self?.showEmptyView() let userDoesntFollowPeople: Bool - if let managedObjectContext = self?.context.managedObjectContext, - let authContext = self?.authContext, - let me = authContext.mastodonAuthenticationBox.authentication.user(in: managedObjectContext){ + if let authContext = self?.authContext, + let me = authContext.mastodonAuthenticationBox.authentication.account() { userDoesntFollowPeople = me.followersCount == 0 } else { userDoesntFollowPeople = true From 94fb3f6c7e71f0ba436bf6e43200a50123108004 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 13 Feb 2024 11:35:25 +0100 Subject: [PATCH 136/159] Update account-screen when following/unfollowing/... (IOS-192) --- .../Scene/Profile/ProfileViewController.swift | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 881f44cae..ce9e968e8 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -863,12 +863,30 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { account: viewModel.account ) - self.viewModel.isUpdating = false self.viewModel.relationship = newRelationship - // TODO: update account? - // TODO: update me? - } + let mastodonAuthenticationBox = self.viewModel.authContext.mastodonAuthenticationBox + let account = self.viewModel.account + if let domain = account.domain, let updatedAccount = try await self.viewModel.context.apiService.fetchUser( + username: self.viewModel.account.acct, + domain: domain, + authenticationBox: mastodonAuthenticationBox + ) { + self.viewModel.account = updatedAccount + } + + let me = self.viewModel.me + if let domain = me.domain, let updatedMe = try? await self.viewModel.context.apiService.fetchUser( + username: me.acct, + domain: domain, + authenticationBox: mastodonAuthenticationBox + ) { + FileManager.default.store(account: updatedMe, forUserID: self.viewModel.authContext.mastodonAuthenticationBox) + self.viewModel.me = updatedMe + } + + self.viewModel.isUpdating = false + } } } From 7024823cbf1a3796aca54e91156d080a1a30b9eb Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 14 Feb 2024 10:44:32 +0100 Subject: [PATCH 137/159] Remove persistence for user (IOS-192) --- .../Share/AuthenticationViewModel.swift | 29 ++-- Mastodon/Scene/Profile/ProfileViewModel.swift | 10 +- .../Persistence+MastodonUser.swift | 151 ------------------ .../Persistence/Persistence+Status.swift | 21 +-- .../Service/API/APIService+Account.swift | 40 ----- .../Service/API/APIService+Reblog.swift | 19 +-- .../Service/API/APIService+Recommend.swift | 49 ------ .../View/Content/StatusView+ViewModel.swift | 2 +- 8 files changed, 28 insertions(+), 293 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift diff --git a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift index 192eefb65..9f4a4ff90 100644 --- a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift @@ -187,7 +187,6 @@ extension AuthenticationViewModel { userToken: Mastodon.Entity.Token ) -> AnyPublisher, Error> { let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken) - let managedObjectContext = context.backgroundManagedObjectContext return context.apiService.accountVerifyCredentials( domain: info.domain, @@ -195,23 +194,21 @@ extension AuthenticationViewModel { ) .tryMap { response -> Mastodon.Response.Content in let account = response.value - let mastodonUserRequest = MastodonUser.sortedFetchRequest - mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id) - mastodonUserRequest.fetchLimit = 1 - guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else { - throw AuthenticationError.badCredentials - } - + + let authentication = MastodonAuthentication.createFrom(domain: info.domain, + userID: account.id, + username: account.username, + appAccessToken: userToken.accessToken, // TODO: swap app token + userAccessToken: userToken.accessToken, + clientID: info.clientID, + clientSecret: info.clientSecret) + AuthenticationServiceProvider.shared .authentications - .insert(MastodonAuthentication.createFrom(domain: info.domain, - userID: mastodonUser.id, - username: mastodonUser.username, - appAccessToken: userToken.accessToken, // TODO: swap app token - userAccessToken: userToken.accessToken, - clientID: info.clientID, - clientSecret: info.clientSecret), at: 0) - + .insert(authentication, at: 0) + + FileManager.default.store(account: account, forUserID: authentication.userIdentifier()) + return response } .eraseToAnyPublisher() diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index a72846451..9a8b567ae 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -182,6 +182,10 @@ class ProfileViewModel: NSObject { let mastodonAuthentication = authContext.mastodonAuthenticationBox.authentication let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) return context.apiService.accountVerifyCredentials(domain: domain, authorization: authorization) + .tryMap { response in + FileManager.default.store(account: response.value, forUserID: mastodonAuthentication.userIdentifier()) + return response + }.eraseToAnyPublisher() } } @@ -226,10 +230,14 @@ extension ProfileViewModel { source: nil, fieldsAttributes: fieldsAttributes ) - return try await context.apiService.accountUpdateCredentials( + let response = try await context.apiService.accountUpdateCredentials( domain: domain, query: query, authorization: authorization ) + + FileManager.default.store(account: response.value, forUserID: authenticationBox.authentication.userIdentifier()) + + return response } } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift deleted file mode 100644 index 16615f7e0..000000000 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+MastodonUser.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// Persistence+MastodonUser.swift -// Persistence+MastodonUser -// -// Created by Cirno MainasuK on 2021-8-18. -// Copyright © 2021 Twidere. All rights reserved. -// - -import CoreData -import CoreDataStack -import Foundation -import MastodonSDK - -extension Persistence.MastodonUser { - - public struct PersistContext { - public let domain: String - public let entity: Mastodon.Entity.Account - public let cache: Persistence.PersistCache? - public let networkDate: Date - - public init( - domain: String, - entity: Mastodon.Entity.Account, - cache: Persistence.PersistCache?, - networkDate: Date - ) { - self.domain = domain - self.entity = entity - self.cache = cache - self.networkDate = networkDate - } - } - - public struct PersistResult { - public let user: MastodonUser - public let isNewInsertion: Bool - - public init( - user: MastodonUser, - isNewInsertion: Bool - ) { - self.user = user - self.isNewInsertion = isNewInsertion - } - } - - public static func createOrMerge( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> PersistResult { - if let oldMastodonUser = fetch(in: managedObjectContext, context: context) { - merge(mastodonUser: oldMastodonUser, context: context) - return PersistResult(user: oldMastodonUser, isNewInsertion: false) - } else { - let user = create(in: managedObjectContext, context: context) - return PersistResult(user: user, isNewInsertion: true) - } - } - -} - -extension Persistence.MastodonUser { - - public static func fetch( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> MastodonUser? { - if let cache = context.cache { - return cache.dictionary[context.entity.id] - } else { - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate( - domain: context.domain, - id: context.entity.id - ) - request.fetchLimit = 1 - do { - return try managedObjectContext.fetch(request).first - } catch { - assertionFailure(error.localizedDescription) - return nil - } - } - } - - @discardableResult - public static func create( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> MastodonUser { - let property = MastodonUser.Property( - entity: context.entity, - domain: context.domain, - networkDate: context.networkDate - ) - let user = MastodonUser.insert(into: managedObjectContext, property: property) - return user - } - - public static func merge( - mastodonUser user: MastodonUser, - context: PersistContext - ) { - guard context.networkDate > user.updatedAt else { return } - let property = MastodonUser.Property( - entity: context.entity, - domain: context.domain, - networkDate: context.networkDate - ) - user.update(property: property) - } -} - -extension Persistence.MastodonUser { - public struct RelationshipContext { - public let entity: Mastodon.Entity.Relationship - public let me: MastodonUser - public let networkDate: Date - - public init( - entity: Mastodon.Entity.Relationship, - me: MastodonUser, - networkDate: Date - ) { - self.entity = entity - self.me = me - self.networkDate = networkDate - } - } - - public static func update( - mastodonUser user: MastodonUser, - context: RelationshipContext - ) { - guard context.entity.id != context.me.id else { return } // not update relationship for self - - let relationship = context.entity - let me = context.me - - user.update(isFollowing: relationship.following, by: me) - user.update(isFollowRequested: relationship.requested, by: me) - // relationship.endorsed.flatMap { user.update(isEndorsed: $0, by: me) } - me.update(isFollowing: relationship.followedBy, by: user) - user.update(isMuting: relationship.muting, by: me) - user.update(isBlocking: relationship.blocking, by: me) - user.update(isDomainBlocking: relationship.domainBlocking, by: me) - me.update(isBlocking: relationship.blockedBy, by: user) - me.update(isShowingReblogs: relationship.showingReblogs, by: user) - } -} diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift index 9767b8b96..79efbf78e 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift @@ -41,16 +41,13 @@ extension Persistence.Status { public struct PersistResult { public let status: Status public let isNewInsertion: Bool - public let isNewInsertionAuthor: Bool public init( status: Status, - isNewInsertion: Bool, - isNewInsertionAuthor: Bool + isNewInsertion: Bool ) { self.status = status self.isNewInsertion = isNewInsertion - self.isNewInsertionAuthor = isNewInsertionAuthor } } @@ -78,8 +75,7 @@ extension Persistence.Status { merge(in: managedObjectContext, mastodonStatus: oldStatus, context: context) return PersistResult( status: oldStatus, - isNewInsertion: false, - isNewInsertionAuthor: false + isNewInsertion: false ) } else { let poll: Poll? = { @@ -98,16 +94,6 @@ extension Persistence.Status { let card = createCard(in: managedObjectContext, context: context) - let authorResult = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: context.domain, - entity: context.entity.account, - cache: context.userCache, - networkDate: context.networkDate - ) - ) - let author = authorResult.user let application: Application? = createApplication(in: managedObjectContext, context: .init(entity: context.entity)) let relationship = Status.Relationship( @@ -124,8 +110,7 @@ extension Persistence.Status { return PersistResult( status: status, - isNewInsertion: true, - isNewInsertionAuthor: authorResult.isNewInsertion + isNewInsertion: true ) } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index a1445f71c..d9d120b2a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -50,33 +50,6 @@ extension APIService { domain: domain, authorization: authorization ) - .flatMap { response -> AnyPublisher, Error> in - let account = response.value - - let managedObjectContext = self.backgroundManagedObjectContext - return managedObjectContext.performChanges { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: account, - cache: nil, - networkDate: response.networkDate - ) - ) - } - .setFailureType(to: Error.self) - .tryMap { result -> Mastodon.Response.Content in - switch result { - case .success: - return response - case .failure(let error): - throw error - } - } - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() } public func accountUpdateCredentials( @@ -91,19 +64,6 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: response.value, - cache: nil, - networkDate: response.networkDate - ) - ) - } - return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index 09ca59a16..55a1828c8 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -62,7 +62,6 @@ extension APIService { query: Mastodon.API.Statuses.RebloggedByQuery, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Account]> { - let managedObjectContext = backgroundManagedObjectContext let statusID: Status.ID = status.reblog?.id ?? status.id let response = try await Mastodon.API.Statuses.rebloggedBy( @@ -72,21 +71,7 @@ extension APIService { query: query, authorization: authenticationBox.userAuthorization ).singleOutput() - - try await managedObjectContext.performChanges { - for entity in response.value { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: .init( - domain: authenticationBox.domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - } // end for … in - } - + return response - } // end func + } } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift index 14255fc82..cd133b89b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Recommend.swift @@ -26,21 +26,6 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - let managedObjectContext = backgroundManagedObjectContext - try await managedObjectContext.performChanges { - for entity in response.value { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: authenticationBox.domain, - entity: entity, - cache: nil, - networkDate: response.networkDate - ) - ) - } // end for … in - } - return response } @@ -55,24 +40,8 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - let managedObjectContext = backgroundManagedObjectContext - try await managedObjectContext.performChanges { - for entity in response.value { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: authenticationBox.domain, - entity: entity.account, - cache: nil, - networkDate: response.networkDate - ) - ) - } // end for … in - } - return response } - } extension APIService { @@ -88,24 +57,6 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput() - let managedObjectContext = backgroundManagedObjectContext - try await managedObjectContext.performChanges { - for entity in response.value { - for account in entity.accounts { - _ = Persistence.MastodonUser.createOrMerge( - in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: authenticationBox.domain, - entity: account, - cache: nil, - networkDate: response.networkDate - ) - ) - - } // end for account in - } // end for entity in - } - return response } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index eef476a38..074ef6736 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -701,7 +701,7 @@ extension StatusView.ViewModel { let menuContext = StatusAuthorView.AuthorMenuContext( name: name, - isMuting: rel.muting ?? false, + isMuting: rel.muting, isBlocking: rel.blocking, isMyself: isMyself, isBookmarking: isBookmark, From cb3a90b5beb31d2790863f7118f8d4725124d009 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 14 Feb 2024 16:51:24 +0100 Subject: [PATCH 138/159] Remove more users (IOS-192) --- .../Root/MainTab/MainTabBarController.swift | 16 +- .../Entity/Mastodon/MastodonUser.swift | 334 ------------------ .../CoreDataStack/MastodonUser+Property.swift | 43 --- 3 files changed, 2 insertions(+), 391 deletions(-) delete mode 100644 MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 2a84c02ef..b32f96000 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -366,20 +366,8 @@ extension MainTabBarController { guard let authContext = authContext else { return } Task { @MainActor in - let profileResponse = try await context.apiService.authenticatedUserInfo( - authenticationBox: authContext.mastodonAuthenticationBox - ) - - if let user = authContext.mastodonAuthenticationBox.authentication.user( - in: context.managedObjectContext - ) { - user.update( - property: .init( - entity: profileResponse.value, - domain: authContext.mastodonAuthenticationBox.domain - ) - ) - } + let profileResponse = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox) + FileManager.default.store(account: profileResponse.value, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) } } } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 807515ec8..7b9c0004e 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -143,20 +143,6 @@ extension MastodonUser { } } -extension MastodonUser { - - @discardableResult - public static func insert( - into context: NSManagedObjectContext, - property: Property - ) -> MastodonUser { - let object: MastodonUser = context.insertObject() - object.configure(property: property) - return object - } - -} - extension MastodonUser: Managed { public static var defaultSortDescriptors: [NSSortDescriptor] { return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)] @@ -209,324 +195,4 @@ extension MastodonUser { public static func predicate(followRequestedBy userID: String) -> NSPredicate { NSPredicate(format: "ANY %K.%K == %@", #keyPath(MastodonUser.followRequestedBy), #keyPath(MastodonUser.id), userID) } - -} - -// MARK: - AutoGenerateProperty -extension MastodonUser: AutoGenerateProperty { - // sourcery:inline:MastodonUser.AutoGenerateProperty - - // Generated using Sourcery - // DO NOT EDIT - public struct Property { - public let identifier: String - public let domain: String - public let id: String - public let acct: String - public let username: String - public let displayName: String - public let avatar: String - public let avatarStatic: String? - public let header: String - public let headerStatic: String? - public let note: String? - public let url: String? - public let statusesCount: Int64 - public let followingCount: Int64 - public let followersCount: Int64 - public let locked: Bool - public let bot: Bool - public let suspended: Bool - public let createdAt: Date - public let updatedAt: Date - public let emojis: [MastodonEmoji] - public let fields: [MastodonField] - - public init( - identifier: String, - domain: String, - id: String, - acct: String, - username: String, - displayName: String, - avatar: String, - avatarStatic: String?, - header: String, - headerStatic: String?, - note: String?, - url: String?, - statusesCount: Int64, - followingCount: Int64, - followersCount: Int64, - locked: Bool, - bot: Bool, - suspended: Bool, - createdAt: Date, - updatedAt: Date, - emojis: [MastodonEmoji], - fields: [MastodonField] - ) { - self.identifier = identifier - self.domain = domain - self.id = id - self.acct = acct - self.username = username - self.displayName = displayName - self.avatar = avatar - self.avatarStatic = avatarStatic - self.header = header - self.headerStatic = headerStatic - self.note = note - self.url = url - self.statusesCount = statusesCount - self.followingCount = followingCount - self.followersCount = followersCount - self.locked = locked - self.bot = bot - self.suspended = suspended - self.createdAt = createdAt - self.updatedAt = updatedAt - self.emojis = emojis - self.fields = fields - } - } - - public func configure(property: Property) { - self.identifier = property.identifier - self.domain = property.domain - self.id = property.id - self.acct = property.acct - self.username = property.username - self.displayName = property.displayName - self.avatar = property.avatar - self.avatarStatic = property.avatarStatic - self.header = property.header - self.headerStatic = property.headerStatic - self.note = property.note - self.url = property.url - self.statusesCount = property.statusesCount - self.followingCount = property.followingCount - self.followersCount = property.followersCount - self.locked = property.locked - self.bot = property.bot - self.suspended = property.suspended - self.createdAt = property.createdAt - self.updatedAt = property.updatedAt - self.emojis = property.emojis - self.fields = property.fields - } - - public func update(property: Property) { - update(acct: property.acct) - update(username: property.username) - update(displayName: property.displayName) - update(avatar: property.avatar) - update(avatarStatic: property.avatarStatic) - update(header: property.header) - update(headerStatic: property.headerStatic) - update(note: property.note) - update(url: property.url) - update(statusesCount: property.statusesCount) - update(followingCount: property.followingCount) - update(followersCount: property.followersCount) - update(locked: property.locked) - update(bot: property.bot) - update(suspended: property.suspended) - update(createdAt: property.createdAt) - update(updatedAt: property.updatedAt) - update(emojis: property.emojis) - update(fields: property.fields) - } - // sourcery:end -} - -// MARK: - AutoUpdatableObject -extension MastodonUser: AutoUpdatableObject { - // sourcery:inline:MastodonUser.AutoUpdatableObject - - // Generated using Sourcery - // DO NOT EDIT - public func update(acct: String) { - if self.acct != acct { - self.acct = acct - } - } - public func update(username: String) { - if self.username != username { - self.username = username - } - } - public func update(displayName: String) { - if self.displayName != displayName { - self.displayName = displayName - } - } - public func update(avatar: String) { - if self.avatar != avatar { - self.avatar = avatar - } - } - public func update(avatarStatic: String?) { - if self.avatarStatic != avatarStatic { - self.avatarStatic = avatarStatic - } - } - public func update(header: String) { - if self.header != header { - self.header = header - } - } - public func update(headerStatic: String?) { - if self.headerStatic != headerStatic { - self.headerStatic = headerStatic - } - } - public func update(note: String?) { - if self.note != note { - self.note = note - } - } - public func update(url: String?) { - if self.url != url { - self.url = url - } - } - public func update(statusesCount: Int64) { - if self.statusesCount != statusesCount { - self.statusesCount = statusesCount - } - } - public func update(followingCount: Int64) { - if self.followingCount != followingCount { - self.followingCount = followingCount - } - } - public func update(followersCount: Int64) { - if self.followersCount != followersCount { - self.followersCount = followersCount - } - } - public func update(locked: Bool) { - if self.locked != locked { - self.locked = locked - } - } - public func update(bot: Bool) { - if self.bot != bot { - self.bot = bot - } - } - public func update(suspended: Bool) { - if self.suspended != suspended { - self.suspended = suspended - } - } - public func update(createdAt: Date) { - if self.createdAt != createdAt { - self.createdAt = createdAt - } - } - public func update(updatedAt: Date) { - if self.updatedAt != updatedAt { - self.updatedAt = updatedAt - } - } - public func update(emojis: [MastodonEmoji]) { - if self.emojis != emojis { - self.emojis = emojis - } - } - public func update(fields: [MastodonField]) { - if self.fields != fields { - self.fields = fields - } - } - // sourcery:end - - public func update(isFollowing: Bool, by mastodonUser: MastodonUser) { - if isFollowing { - if !self.followingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).add(mastodonUser) - } - } else { - if self.followingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.followingBy)).remove(mastodonUser) - } - } - } - public func update(isFollowRequested: Bool, by mastodonUser: MastodonUser) { - if isFollowRequested { - if !self.followRequestedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).add(mastodonUser) - } - } else { - if self.followRequestedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.followRequestedBy)).remove(mastodonUser) - } - } - } - public func update(isMuting: Bool, by mastodonUser: MastodonUser) { - if isMuting { - if !self.mutingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).add(mastodonUser) - } - } else { - if self.mutingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.mutingBy)).remove(mastodonUser) - } - } - } - public func update(isBlocking: Bool, by mastodonUser: MastodonUser) { - if isBlocking { - if !self.blockingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).add(mastodonUser) - } - } else { - if self.blockingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.blockingBy)).remove(mastodonUser) - } - } - } - public func update(isEndorsed: Bool, by mastodonUser: MastodonUser) { - if isEndorsed { - if !self.endorsedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).add(mastodonUser) - } - } else { - if self.endorsedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.endorsedBy)).remove(mastodonUser) - } - } - } - - public func update(isDomainBlocking: Bool, by mastodonUser: MastodonUser) { - if isDomainBlocking { - if !self.domainBlockingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).add(mastodonUser) - } - } else { - if self.domainBlockingBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.domainBlockingBy)).remove(mastodonUser) - } - } - } - - public func update(isShowingReblogs: Bool, by mastodonUser: MastodonUser) { - if isShowingReblogs { - if !self.showingReblogsBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).add(mastodonUser) - } - } else { - if self.showingReblogsBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(MastodonUser.showingReblogsBy)).remove(mastodonUser) - } - } - } -} - -extension MastodonUser { - public var verifiedLink: MastodonField? { - let firstVerified = fields.first(where: { $0.verifiedAt != nil }) - return firstVerified - } } diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift deleted file mode 100644 index 8d2f77ba7..000000000 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// MastodonUser+Property.swift -// Mastodon -// -// Created by MainasuK on 2022-1-11. -// - -import Foundation -import CoreDataStack -import MastodonSDK - -extension MastodonUser.Property { - public init(entity: Mastodon.Entity.Account, domain: String) { - self.init(entity: entity, domain: domain, networkDate: Date()) - } - - init(entity: Mastodon.Entity.Account, domain: String, networkDate: Date) { - self.init( - identifier: entity.id + "@" + domain, - domain: domain, - id: entity.id, - acct: entity.acct, - username: entity.username, - displayName: entity.displayName, - avatar: entity.avatar, - avatarStatic: entity.avatarStatic, - header: entity.header, - headerStatic: entity.headerStatic, - note: entity.note, - url: entity.url, - statusesCount: Int64(entity.statusesCount), - followingCount: Int64(entity.followingCount), - followersCount: Int64(entity.followersCount), - locked: entity.locked, - bot: entity.bot ?? false, - suspended: entity.suspended ?? false, - createdAt: entity.createdAt, - updatedAt: networkDate, - emojis: entity.mastodonEmojis, - fields: entity.mastodonFields - ) - } -} From 68e29d2aea43810dd0490eda1fb81c7aed282247 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 14 Feb 2024 16:52:05 +0100 Subject: [PATCH 139/159] Remove shadow --- .../Header/View/ProfileHeaderView+ViewModel.swift | 2 +- .../Scene/Profile/Header/View/ProfileHeaderView.swift | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 0aa45ae14..a292e75ea 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -267,7 +267,7 @@ extension ProfileHeaderView.ViewModel { .store(in: &disposeBag) // relationship $isRelationshipActionButtonHidden - .assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer) + .assign(to: \.isHidden, on: view.relationshipActionButton) .store(in: &disposeBag) Publishers.CombineLatest3( diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 7b0190b10..046b74876 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -194,7 +194,6 @@ final class ProfileHeaderView: UIView { let statusDashboardView = ProfileStatusDashboardView() - let relationshipActionButtonShadowContainer = ShadowBackgroundContainer() let relationshipActionButton: ProfileRelationshipActionButton = { let button = ProfileRelationshipActionButton() button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) @@ -369,7 +368,7 @@ final class ProfileHeaderView: UIView { avatarImageViewBackgroundView.bottomAnchor.constraint(equalTo: dashboardContainer.bottomAnchor), ]) - // authorContainer: H - [ nameContainer | padding | relationshipActionButtonShadowContainer ] + // authorContainer: H - [ nameContainer | padding | relationshipActionButton ] let authorContainer = UIStackView() authorContainer.axis = .horizontal authorContainer.alignment = .top @@ -420,11 +419,9 @@ final class ProfileHeaderView: UIView { authorContainer.addArrangedSubview(nameContainerStackView) authorContainer.addArrangedSubview(UIView()) - authorContainer.addArrangedSubview(relationshipActionButtonShadowContainer) - + authorContainer.addArrangedSubview(relationshipActionButton) + relationshipActionButton.translatesAutoresizingMaskIntoConstraints = false - relationshipActionButtonShadowContainer.addSubview(relationshipActionButton) - relationshipActionButton.pinToParent() NSLayoutConstraint.activate([ relationshipActionButton.widthAnchor.constraint(greaterThanOrEqualToConstant: ProfileHeaderView.friendshipActionButtonSize.width).priority(.required - 1), relationshipActionButton.heightAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.height).priority(.defaultHigh), From 6bcbc0ac07667b6f05a6025fff9867b2191ce519 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Wed, 14 Feb 2024 16:52:27 +0100 Subject: [PATCH 140/159] Remove obsolete code (IOS-192) --- ...omeTimelineViewModel+LoadLatestState.swift | 2 -- Mastodon/Supporting Files/SceneDelegate.swift | 3 --- .../Service/InstanceService.swift | 20 ------------------- 3 files changed, 25 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index ff24f74bd..a88bafebe 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -100,8 +100,6 @@ extension HomeTimelineViewModel.LoadLatestState { await enter(state: Idle.self) viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(.finished) - viewModel.context.instanceService.updateMutesAndBlocks() - // stop refresher if no new statuses let statuses = response.value let newStatuses = statuses.filter { status in !latestStatusIDs.contains(where: { $0 == status.reblog?.id || $0 == status.id }) } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 42c23b433..014c0cedb 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -92,9 +92,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // trigger authenticated user account update AppContext.shared.authenticationService.updateActiveUserAccountPublisher.send() - - // update mutes and blocks and remove related data - AppContext.shared.instanceService.updateMutesAndBlocks() if let shortcutItem = savedShortCutItem { Task { diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift index 0d1509af8..fb4335cc3 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift @@ -121,23 +121,3 @@ extension InstanceService { .eraseToAnyPublisher() } } - -public extension InstanceService { - func updateMutesAndBlocks() { - Task { - for authBox in authenticationService?.mastodonAuthenticationBoxes ?? [] { - do { - try await apiService?.getMutes( - authenticationBox: authBox - ) - - try await apiService?.getBlocked( - authenticationBox: authBox - ) - - } catch { - } - } - } - } -} From 88c5cfa1402b5d9c980b0e7a12e515a73a4795cc Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 15 Feb 2024 12:53:21 +0100 Subject: [PATCH 141/159] Make the app work on iPad again (IOS-192) Friends don't let friends use forced unwrapping. --- ...ineViewController+DataSourceProvider.swift | 4 +- .../HomeTimelineViewController.swift | 86 ++++++++++--------- .../NotificationViewController.swift | 17 ++-- .../Notification/NotificationViewModel.swift | 1 - .../Root/ContentSplitViewController.swift | 4 +- .../Search/Search/SearchViewController.swift | 8 +- 6 files changed, 64 insertions(+), 56 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift index 47a844b28..3a0457983 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift @@ -16,7 +16,7 @@ extension HomeTimelineViewController: DataSourceProvider { } guard let indexPath = _indexPath else { return nil } - guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + guard let item = viewModel?.diffableDataSource?.itemIdentifier(for: indexPath) else { return nil } @@ -34,7 +34,7 @@ extension HomeTimelineViewController: DataSourceProvider { } func update(status: MastodonStatus, intent: MastodonStatus.UpdateIntent) { - viewModel.dataController.update(status: status, intent: intent) + viewModel?.dataController.update(status: status, intent: intent) } @MainActor diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 385ab8ef0..9666bf12e 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -25,8 +25,8 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - var viewModel: HomeTimelineViewModel! - + var viewModel: HomeTimelineViewModel? + let mediaPreviewTransitionController = MediaPreviewTransitionController() let friendsAssetImageView: UIImageView = { @@ -82,7 +82,7 @@ extension HomeTimelineViewController { title = L10n.Scene.HomeTimeline.title view.backgroundColor = .secondarySystemBackground - viewModel.$displaySettingBarButtonItem + viewModel?.$displaySettingBarButtonItem .receive(on: DispatchQueue.main) .sink { [weak self] displaySettingBarButtonItem in guard let self = self else { return } @@ -97,7 +97,7 @@ extension HomeTimelineViewController { navigationItem.titleView = titleView titleView.delegate = self - viewModel.homeTimelineNavigationBarTitleViewModel.state + viewModel?.homeTimelineNavigationBarTitleViewModel.state .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] state in @@ -106,7 +106,7 @@ extension HomeTimelineViewController { } .store(in: &disposeBag) - viewModel.homeTimelineNavigationBarTitleViewModel.state + viewModel?.homeTimelineNavigationBarTitleViewModel.state .removeDuplicates() .filter { $0 == .publishedButton } .receive(on: DispatchQueue.main) @@ -137,27 +137,27 @@ extension HomeTimelineViewController { publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), ]) - viewModel.tableView = tableView + viewModel?.tableView = tableView tableView.delegate = self - viewModel.setupDiffableDataSource( + viewModel?.setupDiffableDataSource( tableView: tableView, statusTableViewCellDelegate: self, timelineMiddleLoaderTableViewCellDelegate: self ) // setup batch fetch - viewModel.listBatchFetchViewModel.setup(scrollView: tableView) - viewModel.listBatchFetchViewModel.shouldFetch + viewModel?.listBatchFetchViewModel.setup(scrollView: tableView) + viewModel?.listBatchFetchViewModel.shouldFetch .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } guard self.view.window != nil else { return } - self.viewModel.loadOldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) + self.viewModel?.loadOldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self) } .store(in: &disposeBag) // bind refresh control - viewModel.didLoadLatest + viewModel?.didLoadLatest .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } @@ -170,8 +170,8 @@ extension HomeTimelineViewController { context.publisherService.statusPublishResult.receive(on: DispatchQueue.main).sink { result in if case .success(.edit(let status)) = result { - self.viewModel.hasPendingStatusEditReload = true - self.viewModel.dataController.update(status: .fromEntity(status.value), intent: .edit) + self.viewModel?.hasPendingStatusEditReload = true + self.viewModel?.dataController.update(status: .fromEntity(status.value), intent: .edit) } }.store(in: &disposeBag) @@ -204,7 +204,7 @@ extension HomeTimelineViewController { } .store(in: &disposeBag) - viewModel.timelineIsEmpty + viewModel?.timelineIsEmpty .receive(on: DispatchQueue.main) .sink { [weak self] isEmpty in if isEmpty { @@ -218,9 +218,9 @@ extension HomeTimelineViewController { userDoesntFollowPeople = true } - if (self?.viewModel.presentedSuggestions == false) && userDoesntFollowPeople { + if (self?.viewModel?.presentedSuggestions == false) && userDoesntFollowPeople { self?.findPeopleButtonPressed(self) - self?.viewModel.presentedSuggestions = true + self?.viewModel?.presentedSuggestions = true } } else { self?.emptyView.removeFromSuperview() @@ -264,16 +264,16 @@ extension HomeTimelineViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if let timestamp = viewModel.lastAutomaticFetchTimestamp { + if let timestamp = viewModel?.lastAutomaticFetchTimestamp { let now = Date() if now.timeIntervalSince(timestamp) > 60 { - self.viewModel.lastAutomaticFetchTimestamp = now - self.viewModel.homeTimelineNeedRefresh.send() + self.viewModel?.lastAutomaticFetchTimestamp = now + self.viewModel?.homeTimelineNeedRefresh.send() } else { // do nothing } } else { - self.viewModel.homeTimelineNeedRefresh.send() + self.viewModel?.homeTimelineNeedRefresh.send() } } @@ -284,7 +284,7 @@ extension HomeTimelineViewController { // do nothing } completion: { _ in // fix AutoLayout cell height not update after rotate issue - self.viewModel.cellFrameCache.removeAllObjects() + self.viewModel?.cellFrameCache.removeAllObjects() self.tableView.reloadData() } } @@ -356,7 +356,9 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { @objc private func findPeopleButtonPressed(_ sender: Any?) { - let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: viewModel.authContext) + guard let authContext = viewModel?.authContext else { return } + + let suggestionAccountViewModel = SuggestionAccountViewModel(context: context, authContext: authContext) suggestionAccountViewModel.delegate = viewModel _ = coordinator.present( scene: .suggestionAccount(viewModel: suggestionAccountViewModel), @@ -366,7 +368,9 @@ extension HomeTimelineViewController { } @objc private func manuallySearchButtonPressed(_ sender: UIButton) { - let searchDetailViewModel = SearchDetailViewModel(authContext: viewModel.authContext) + guard let authContext = viewModel?.authContext else { return } + + let searchDetailViewModel = SearchDetailViewModel(authContext: authContext) _ = coordinator.present(scene: .searchDetail(viewModel: searchDetailViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @@ -377,16 +381,18 @@ extension HomeTimelineViewController { } @objc private func refreshControlValueChanged(_ sender: RefreshControl) { - guard viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.LoadingManually.self) else { + guard let viewModel, viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.LoadingManually.self) else { sender.endRefreshing() return } } @objc func signOutAction(_ sender: UIAction) { + guard let authContext = viewModel?.authContext else { return } + Task { @MainActor in - try await context.authenticationService.signOutMastodonUser(authenticationBox: viewModel.authContext.mastodonAuthenticationBox) - let userIdentifier = viewModel.authContext.mastodonAuthenticationBox + try await context.authenticationService.signOutMastodonUser(authenticationBox: authContext.mastodonAuthenticationBox) + let userIdentifier = authContext.mastodonAuthenticationBox FileManager.default.invalidateHomeTimelineCache(for: userIdentifier) FileManager.default.invalidateNotificationsAll(for: userIdentifier) FileManager.default.invalidateNotificationsMentions(for: userIdentifier) @@ -400,7 +406,7 @@ extension HomeTimelineViewController { func scrollViewDidScroll(_ scrollView: UIScrollView) { switch scrollView { case tableView: - viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView) + viewModel?.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView) default: break } @@ -411,7 +417,7 @@ extension HomeTimelineViewController { case tableView: let indexPath = IndexPath(row: 0, section: 0) - guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { + guard viewModel?.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return true } // save position @@ -428,7 +434,7 @@ extension HomeTimelineViewController { private func savePositionBeforeScrollToTop() { // check save action interval // should not fast than 0.5s to prevent save when scrollToTop on-flying - if let record = viewModel.scrollPositionRecord { + if let record = viewModel?.scrollPositionRecord { let now = Date() guard now.timeIntervalSince(record.timestamp) > 0.5 else { // skip this save action @@ -436,7 +442,7 @@ extension HomeTimelineViewController { } } - guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let diffableDataSource = viewModel?.diffableDataSource else { return } guard let anchorIndexPaths = tableView.indexPathsForVisibleRows?.sorted() else { return } guard !anchorIndexPaths.isEmpty else { return } let anchorIndexPath = anchorIndexPaths[anchorIndexPaths.count / 2] @@ -447,7 +453,7 @@ extension HomeTimelineViewController { let cellFrameInView = tableView.convert(anchorCell.frame, to: view) return cellFrameInView.origin.y }() - viewModel.scrollPositionRecord = HomeTimelineViewModel.ScrollPositionRecord( + viewModel?.scrollPositionRecord = HomeTimelineViewModel.ScrollPositionRecord( item: anchorItem, offset: offset, timestamp: Date() @@ -462,19 +468,19 @@ extension HomeTimelineViewController { } private func restorePositionWhenScrollToTop() { - guard let diffableDataSource = self.viewModel.diffableDataSource else { return } - guard let record = self.viewModel.scrollPositionRecord, + guard let diffableDataSource = viewModel?.diffableDataSource else { return } + guard let record = viewModel?.scrollPositionRecord, let indexPath = diffableDataSource.indexPath(for: record.item) else { return } tableView.scrollToRow(at: indexPath, at: .middle, animated: true) - viewModel.scrollPositionRecord = nil + viewModel?.scrollPositionRecord = nil } } // MARK: - AuthContextProvider extension HomeTimelineViewController: AuthContextProvider { - var authContext: AuthContext { viewModel.authContext } + var authContext: AuthContext { viewModel!.authContext } } // MARK: - UITableViewDelegate @@ -507,7 +513,7 @@ extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableView func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - viewModel.timelineDidReachEnd() + viewModel?.timelineDidReachEnd() } } } @@ -515,12 +521,12 @@ extension HomeTimelineViewController: UITableViewDelegate, AutoGenerateTableView // MARK: - TimelineMiddleLoaderTableViewCellDelegate extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate { func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let diffableDataSource = viewModel?.diffableDataSource else { return } guard let indexPath = tableView.indexPath(for: cell) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } Task { - await viewModel.loadMore(item: item) + await viewModel?.loadMore(item: item) } } } @@ -531,6 +537,8 @@ extension HomeTimelineViewController: ScrollViewContainer { var scrollView: UIScrollView { return tableView } func scrollToTop(animated: Bool) { + guard let viewModel else { return } + if scrollView.contentOffset.y < scrollView.frame.height, viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self), (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0, @@ -569,7 +577,7 @@ extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate func homeTimelineNavigationBarTitleView(_ titleView: HomeTimelineNavigationBarTitleView, buttonDidPressed sender: UIButton) { switch titleView.state { case .newPostButton: - guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let diffableDataSource = viewModel?.diffableDataSource else { return } let indexPath = IndexPath(row: 0, section: 0) guard diffableDataSource.itemIdentifier(for: indexPath) != nil else { return } diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index a266049bd..f3f48a1ec 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -21,7 +21,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency { var disposeBag = Set() var observations = Set() - var viewModel: NotificationViewModel! + var viewModel: NotificationViewModel? let pageSegmentedControl = UISegmentedControl() @@ -38,7 +38,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency { animated: animated ) - viewModel.currentPageIndex = index + viewModel?.currentPageIndex = index } } @@ -49,7 +49,7 @@ extension NotificationViewController { view.backgroundColor = .secondarySystemBackground - setupSegmentedControl(scopes: viewModel.scopes) + setupSegmentedControl(scopes: APIService.MastodonNotificationScope.allCases) pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false navigationItem.titleView = pageSegmentedControl NSLayoutConstraint.activate([ @@ -58,7 +58,7 @@ extension NotificationViewController { pageSegmentedControl.addTarget(self, action: #selector(NotificationViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged) dataSource = viewModel - viewModel.$viewControllers + viewModel?.$viewControllers .receive(on: DispatchQueue.main) .sink { [weak self] viewControllers in guard let self = self else { return } @@ -68,11 +68,11 @@ extension NotificationViewController { } .store(in: &disposeBag) - viewModel.viewControllers = viewModel.scopes.map { scope in + viewModel?.viewControllers = APIService.MastodonNotificationScope.allCases.map { scope in createViewController(for: scope) } - viewModel.$currentPageIndex + viewModel?.$currentPageIndex .receive(on: DispatchQueue.main) .sink { [weak self] currentPageIndex in guard let self = self else { return } @@ -127,7 +127,7 @@ extension NotificationViewController { } // set initial selection - guard !pageSegmentedControl.isSelected else { return } + guard let viewModel, !pageSegmentedControl.isSelected else { return } if viewModel.currentPageIndex < pageSegmentedControl.numberOfSegments { pageSegmentedControl.selectedSegmentIndex = viewModel.currentPageIndex } else { @@ -136,12 +136,13 @@ extension NotificationViewController { } private func createViewController(for scope: NotificationTimelineViewModel.Scope) -> UIViewController { + guard let authContext = viewModel?.authContext else { return UITableViewController() } let viewController = NotificationTimelineViewController() viewController.context = context viewController.coordinator = coordinator viewController.viewModel = NotificationTimelineViewModel( context: context, - authContext: viewModel.authContext, + authContext: authContext, scope: scope ) return viewController diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index 39c1b9ca5..8fa48da2f 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -22,7 +22,6 @@ final class NotificationViewModel { let viewDidLoad = PassthroughSubject() // output - let scopes = NotificationTimelineViewModel.Scope.allCases @Published var viewControllers: [UIViewController] = [] @Published var currentPageIndex = 0 { didSet { diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 68294739e..197988b4b 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -39,9 +39,9 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { @Published var currentSupplementaryTab: Tab = .home private(set) lazy var mainTabBarController: MainTabBarController = { - let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator, authContext: authContext) + let mainTabBarController = MainTabBarController(context: self.context, coordinator: self.coordinator, authContext: self.authContext) if let homeTimelineViewController = mainTabBarController.viewController(of: HomeTimelineViewController.self) { - homeTimelineViewController.viewModel.displaySettingBarButtonItem = false + homeTimelineViewController.viewModel?.displaySettingBarButtonItem = false } return mainTabBarController }() diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 0f2034a17..9af36edd1 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -26,7 +26,7 @@ final class SearchViewController: UIViewController, NeedsDependency { var searchTransitionController = SearchTransitionController() var disposeBag = Set() - var viewModel: SearchViewModel! + var viewModel: SearchViewModel? // use AutoLayout could set search bar margin automatically to // layout alongside with split mode button (on iPad) @@ -37,7 +37,7 @@ final class SearchViewController: UIViewController, NeedsDependency { let searchBarTapPublisher = PassthroughSubject() private(set) lazy var discoveryViewController: DiscoveryViewController? = { - guard let authContext = viewModel.authContext else { return nil } + guard let authContext = viewModel?.authContext else { return nil } let viewController = DiscoveryViewController() viewController.context = context viewController.coordinator = coordinator @@ -70,7 +70,7 @@ extension SearchViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppeared.send() + viewModel?.viewDidAppeared.send() // note: // need set alpha because (maybe) SDK forget set alpha back @@ -110,7 +110,7 @@ extension SearchViewController { .sink { [weak self] initialText in guard let self = self else { return } // push to search detail - guard let authContext = self.viewModel.authContext else { return } + guard let authContext = self.viewModel?.authContext else { return } let searchDetailViewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: initialText) searchDetailViewModel.needsBecomeFirstResponder = true self.navigationController?.delegate = self.searchTransitionController From 6f16a997594bc24b29db703d44995f702b9e238b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 15 Feb 2024 16:24:49 +0100 Subject: [PATCH 142/159] Update accounts on profile-screen on pull to refresh (IOS-192) --- .../Scene/Profile/ProfileViewController.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index ce9e968e8..4383f21ce 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -577,11 +577,26 @@ extension ProfileViewController { userTimelineViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self) } - // trigger authenticated user account update - viewModel.context.authenticationService.updateActiveUserAccountPublisher.send() + Task { + let account = viewModel.account + if let domain = account.domain, + let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox), + let updatedRelationship = try? await context.apiService.relationship(forAccounts: [updatedAccount], authenticationBox: authContext.mastodonAuthenticationBox).value.first + { + viewModel.account = updatedAccount + viewModel.relationship = updatedRelationship + } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - sender.endRefreshing() + let me = viewModel.me + + if let domain = me.domain, let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { + viewModel.me = updatedMe + FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + sender.endRefreshing() + } } } From 04e3a573a02949af8bb74defa7b407fb13926718 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Thu, 15 Feb 2024 17:41:39 +0100 Subject: [PATCH 143/159] Remove unused code (IOS-192) --- Mastodon/Scene/Profile/ProfileViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 4383f21ce..e0dc68356 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -589,7 +589,7 @@ extension ProfileViewController { let me = viewModel.me - if let domain = me.domain, let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { + if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { viewModel.me = updatedMe FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) } From ecc4535058cb4e16ee43ed1bf59a5aa16b8a83e4 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 16 Feb 2024 09:19:16 +0100 Subject: [PATCH 144/159] Update screen when relationship changes on another screen (IOS-192) --- .../Scene/Profile/ProfileViewController.swift | 76 ++++++++++++++++--- .../Service/API/APIService+HomeTimeline.swift | 1 + 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index e0dc68356..ed0487847 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -166,6 +166,8 @@ extension ProfileViewController { override func viewDidLoad() { super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(ProfileViewController.relationshipChanged(_:)), name: .relationshipChanged, object: nil) + view.backgroundColor = .secondarySystemBackground let barAppearance = UINavigationBarAppearance() if isModal { @@ -809,7 +811,13 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { ) self.viewModel.isUpdating = false - self.viewModel.relationship = newRelationship + let userInfo = [ + "account": self.viewModel.account, + "relationship": newRelationship, + "me": self.viewModel.me + ] + + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) } } alertController.addAction(unblockAction) @@ -828,15 +836,19 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let unblockAction = UIAlertAction(title: L10n.Common.Controls.Actions.unblockDomain(domain), style: .default) { [weak self] _ in guard let self else { return } Task { - _ = try await DataSourceFacade.responseToDomainBlockAction( - dependency: self, - account: account - ) + _ = try await DataSourceFacade.responseToDomainBlockAction(dependency: self, account: account) guard let newRelationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: self.authContext.mastodonAuthenticationBox).value.first else { return } self.viewModel.isUpdating = false - self.viewModel.relationship = newRelationship + + let userInfo = [ + "account": self.viewModel.account, + "relationship": newRelationship, + "me": self.viewModel.me + ] + + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) } } alertController.addAction(unblockAction) @@ -856,13 +868,18 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in guard let self else { return } Task { - let newRelationship = try await DataSourceFacade.responseToUserMuteAction( - dependency: self, - account: account - ) - + + let newRelationship = try await DataSourceFacade.responseToUserMuteAction(dependency: self, account: account) + self.viewModel.isUpdating = false - self.viewModel.relationship = newRelationship + let userInfo = [ + "account": self.viewModel.account, + "relationship": newRelationship, + "me": self.viewModel.me + ] + + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) + } } alertController.addAction(unmuteAction) @@ -900,6 +917,16 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { self.viewModel.me = updatedMe } + self.viewModel.isUpdating = false + let userInfo = [ + "account": self.viewModel.account, + "relationship": newRelationship, + "me": self.viewModel.me + ] + + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) + + self.viewModel.isUpdating = false } } @@ -1029,3 +1056,28 @@ extension ProfileViewController: DataSourceProvider { viewModel.mediaUserTimelineViewModel.dataController.update(status: status, intent: intent) } } + +//MARK: - Notifications + +extension ProfileViewController { + @objc + func relationshipChanged(_ notification: Notification) { + + guard let userInfo = notification.userInfo, + let account = userInfo["account"] as? Mastodon.Entity.Account, + let me = userInfo["me"] as? Mastodon.Entity.Account, + let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship else { + return + } + + if account == viewModel.account { + viewModel.account = account + } + + if me == viewModel.me { + viewModel.me = me + } + + viewModel.relationship = relationship + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 1b32b88aa..976ccfb73 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -13,6 +13,7 @@ import MastodonSDK public extension Foundation.Notification.Name { static let userFetched = Notification.Name(rawValue: "org.joinmastodon.app.user-fetched") + static let relationshipChanged = Notification.Name(rawValue: "org.joinmastodon.app.relationship-changed") } extension APIService { From 03e9ed7e80b2a549a5e5333b6ffa5823ad29ee0c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 16 Feb 2024 09:19:59 +0100 Subject: [PATCH 145/159] FIx warning --- Mastodon/Scene/Profile/ProfileViewController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index ed0487847..d99c6cd5d 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -589,8 +589,6 @@ extension ProfileViewController { viewModel.relationship = updatedRelationship } - let me = viewModel.me - if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { viewModel.me = updatedMe FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) From a1ba1898225002760c51a55fd709d81cced97b97 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 16 Feb 2024 09:30:52 +0100 Subject: [PATCH 146/159] Update profile-screen when relationship changes on user list (IOS-192) --- Mastodon/Scene/Profile/ProfileViewController.swift | 13 ++++++------- .../TableviewCell/UserTableViewCell+ViewModel.swift | 13 ++++++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index d99c6cd5d..50efca93d 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -1061,21 +1061,20 @@ extension ProfileViewController { @objc func relationshipChanged(_ notification: Notification) { - guard let userInfo = notification.userInfo, - let account = userInfo["account"] as? Mastodon.Entity.Account, - let me = userInfo["me"] as? Mastodon.Entity.Account, - let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship else { + guard let userInfo = notification.userInfo else { return } - if account == viewModel.account { + if let account = userInfo["account"] as? Mastodon.Entity.Account, account == viewModel.account { viewModel.account = account } - if me == viewModel.me { + if let me = userInfo["me"] as? Mastodon.Entity.Account, me == viewModel.me { viewModel.me = me } - viewModel.relationship = relationship + if let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship { + viewModel.relationship = relationship + } } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index ffb927166..86daa2c21 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -62,7 +62,10 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro // Otherwise the relationship might still be `pending` try await Task.sleep(for: .seconds(1)) - let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first + guard let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first, + let updatedAccount = try await context.apiService.fetchUser(username: account.acct, domain: account.domain ?? "", authenticationBox: authContext.mastodonAuthenticationBox) else { return } + + let updatedMe = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value let isMe: Bool if let me { @@ -74,6 +77,14 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro await MainActor.run { view.viewModel.relationship = relationship view.updateButtonState(with: relationship, isMe: isMe) + + let userInfo = [ + "account": updatedAccount, + "relationship": relationship, + "me": updatedMe + ] + + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) } } From ffb5c59d12c83a991fbd73245830dbb949ffaa6c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 16 Feb 2024 10:34:28 +0100 Subject: [PATCH 147/159] Slighty refactor relationship-update-notification-handling (IOS-192) Also: consider menu-changes from profile-screen --- .../Scene/Profile/ProfileViewController.swift | 36 +++++++++---------- .../UserTableViewCell+ViewModel.swift | 17 ++++----- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 50efca93d..525e93827 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -810,9 +810,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { self.viewModel.isUpdating = false let userInfo = [ - "account": self.viewModel.account, "relationship": newRelationship, - "me": self.viewModel.me ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) @@ -841,9 +839,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { self.viewModel.isUpdating = false let userInfo = [ - "account": self.viewModel.account, "relationship": newRelationship, - "me": self.viewModel.me ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) @@ -871,9 +867,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { self.viewModel.isUpdating = false let userInfo = [ - "account": self.viewModel.account, "relationship": newRelationship, - "me": self.viewModel.me ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) @@ -917,9 +911,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { self.viewModel.isUpdating = false let userInfo = [ - "account": self.viewModel.account, "relationship": newRelationship, - "me": self.viewModel.me ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) @@ -970,9 +962,9 @@ extension ProfileViewController: MastodonMenuDelegate { button: nil, barButtonItem: self.moreMenuBarButtonItem )) { [weak self] newRelationship in - guard let self else { return } - - self.viewModel.relationship = newRelationship + NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ + "relationship": newRelationship + ]) } } case .reportUser(_), .shareUser(_): @@ -1061,20 +1053,24 @@ extension ProfileViewController { @objc func relationshipChanged(_ notification: Notification) { - guard let userInfo = notification.userInfo else { + guard let userInfo = notification.userInfo, let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship, viewModel.account.id == relationship.id else { return } - if let account = userInfo["account"] as? Mastodon.Entity.Account, account == viewModel.account { - viewModel.account = account - } + Task { + viewModel.isUpdating = true + let account = viewModel.account + if let domain = account.domain, + let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox) { + viewModel.account = updatedAccount + } - if let me = userInfo["me"] as? Mastodon.Entity.Account, me == viewModel.me { - viewModel.me = me - } - - if let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship { + if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { + viewModel.me = updatedMe + FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) + } viewModel.relationship = relationship + viewModel.isUpdating = false } } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 86daa2c21..657296009 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -62,10 +62,7 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro // Otherwise the relationship might still be `pending` try await Task.sleep(for: .seconds(1)) - guard let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first, - let updatedAccount = try await context.apiService.fetchUser(username: account.acct, domain: account.domain ?? "", authenticationBox: authContext.mastodonAuthenticationBox) else { return } - - let updatedMe = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value + let relationship = try await self.context.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first let isMe: Bool if let me { @@ -78,13 +75,13 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro view.viewModel.relationship = relationship view.updateButtonState(with: relationship, isMe: isMe) - let userInfo = [ - "account": updatedAccount, - "relationship": relationship, - "me": updatedMe - ] + if let relationship { + let userInfo = [ + "relationship": relationship, + ] - NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) + } } } From 91e3b63f1f33cd408df779434ce3596699bace2a Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Fri, 16 Feb 2024 12:16:55 +0100 Subject: [PATCH 148/159] Rework relationship-notification (IOS-192) They get sent from the DataSourceFacade now --- .../Provider/DataSourceFacade+Block.swift | 8 +- .../Provider/DataSourceFacade+Follow.swift | 19 +++- .../Provider/DataSourceFacade+Mute.swift | 6 ++ .../Provider/DataSourceFacade+Status.swift | 8 +- .../Provider/DataSourceFacade+UserView.swift | 33 +------ ...er+NotificationTableViewCellDelegate.swift | 1 - .../Scene/Profile/ProfileViewController.swift | 98 ++++++------------- .../UserTableViewCell+ViewModel.swift | 9 -- 8 files changed, 62 insertions(+), 120 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index 3473d6e80..02bf7893b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -26,11 +26,11 @@ extension DataSourceFacade { authenticationBox: authBox ) - try await dependency.context.apiService.getBlocked( - authenticationBox: authBox - ) + let userInfo = [ + "relationship": response.value, + ] - dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) return response.value } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 249ff5358..2c16c8241 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -26,6 +26,10 @@ extension DataSourceFacade { dependency.context.authenticationService.fetchFollowingAndBlockedAsync() + NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ + "relationship": response + ]) + return response } @@ -70,6 +74,10 @@ extension DataSourceFacade { break } + NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ + "relationship": newRelationship + ]) + await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox) } catch { // reset state when failure @@ -101,8 +109,15 @@ extension DataSourceFacade { dependency: NeedsDependency & AuthContextProvider, account: Mastodon.Entity.Account ) async throws { - _ = try await dependency.context.apiService.toggleShowReblogs( + let newRelationship = try await dependency.context.apiService.toggleShowReblogs( for: account, - authenticationBox: dependency.authContext.mastodonAuthenticationBox) + authenticationBox: dependency.authContext.mastodonAuthenticationBox + ) + + let userInfo = [ + "relationship": newRelationship, + ] + + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 68462e462..3e70d7b09 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -22,6 +22,12 @@ extension DataSourceFacade { account: account ) + let userInfo = [ + "relationship": response.value, + ] + + NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) + return response.value } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 87d1c90f9..dbfc931ba 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -199,8 +199,8 @@ extension DataSourceFacade { account: menuContext.author ) - if let completion { - completion(newRelationship as! T) + if let completion, let relationship = newRelationship as? T { + completion(relationship) } } } @@ -225,8 +225,8 @@ extension DataSourceFacade { account: menuContext.author ) - if let completion { - completion(newRelationship as! T) + if let completion, let relationship = newRelationship as? T { + completion(relationship) } } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift index 097f430b5..de70e6304 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift @@ -13,42 +13,11 @@ extension DataSourceFacade { buttonState: UserView.ButtonState ) async throws { switch buttonState { - case .follow: + case .follow, .request, .unfollow, .blocked, .pending: _ = try await DataSourceFacade.responseToUserFollowAction( dependency: dependency, account: account ) - - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.append(account.id) - case .request: - _ = try await DataSourceFacade.responseToUserFollowAction( - dependency: dependency, - account: account - ) - - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.append(account.id) - case .unfollow: - _ = try await DataSourceFacade.responseToUserFollowAction( - dependency: dependency, - account: account - ) - - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds.removeAll(where: { $0 == account.id }) - case .blocked: - _ = try await DataSourceFacade.responseToUserBlockAction( - dependency: dependency, - account: account - ) - - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds.append(account.id) - - case .pending: - _ = try await DataSourceFacade.responseToUserFollowAction( - dependency: dependency, - account: account - ) - - dependency.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs.removeAll(where: { $0 == account.id }) case .none, .loading: break //no-op } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index c7b769f4c..2e9e7ad50 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -115,7 +115,6 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut return } - try await DataSourceFacade.responseToUserFollowRequestAction( dependency: self, notification: notification, diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 525e93827..47c93d3c1 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -807,13 +807,6 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { dependency: self, account: account ) - - self.viewModel.isUpdating = false - let userInfo = [ - "relationship": newRelationship, - ] - - NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) } } alertController.addAction(unblockAction) @@ -838,6 +831,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { self.viewModel.isUpdating = false + // we need to trigger this here as domain block doesn't return a relationship let userInfo = [ "relationship": newRelationship, ] @@ -862,16 +856,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in guard let self else { return } Task { - - let newRelationship = try await DataSourceFacade.responseToUserMuteAction(dependency: self, account: account) - - self.viewModel.isUpdating = false - let userInfo = [ - "relationship": newRelationship, - ] - - NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) - + _ = try await DataSourceFacade.responseToUserMuteAction(dependency: self, account: account) } } alertController.addAction(unmuteAction) @@ -882,42 +867,10 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { Task { [weak self] in guard let self else { return } - let newRelationship = try await DataSourceFacade.responseToUserFollowAction( + _ = try await DataSourceFacade.responseToUserFollowAction( dependency: self, account: viewModel.account ) - - self.viewModel.relationship = newRelationship - let mastodonAuthenticationBox = self.viewModel.authContext.mastodonAuthenticationBox - - let account = self.viewModel.account - if let domain = account.domain, let updatedAccount = try await self.viewModel.context.apiService.fetchUser( - username: self.viewModel.account.acct, - domain: domain, - authenticationBox: mastodonAuthenticationBox - ) { - self.viewModel.account = updatedAccount - } - - let me = self.viewModel.me - if let domain = me.domain, let updatedMe = try? await self.viewModel.context.apiService.fetchUser( - username: me.acct, - domain: domain, - authenticationBox: mastodonAuthenticationBox - ) { - FileManager.default.store(account: updatedMe, forUserID: self.viewModel.authContext.mastodonAuthenticationBox) - self.viewModel.me = updatedMe - } - - self.viewModel.isUpdating = false - let userInfo = [ - "relationship": newRelationship, - ] - - NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) - - - self.viewModel.isUpdating = false } } } @@ -961,11 +914,7 @@ extension ProfileViewController: MastodonMenuDelegate { statusViewModel: nil, button: nil, barButtonItem: self.moreMenuBarButtonItem - )) { [weak self] newRelationship in - NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ - "relationship": newRelationship - ]) - } + )) } case .reportUser(_), .shareUser(_): Task { @@ -1053,24 +1002,37 @@ extension ProfileViewController { @objc func relationshipChanged(_ notification: Notification) { - guard let userInfo = notification.userInfo, let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship, viewModel.account.id == relationship.id else { + guard let userInfo = notification.userInfo, let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship else { return } - Task { - viewModel.isUpdating = true - let account = viewModel.account - if let domain = account.domain, - let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox) { - viewModel.account = updatedAccount - } + viewModel.isUpdating = true + if viewModel.account.id == relationship.id { + // if relationship belongs to an other account + Task { + let account = viewModel.account + if let domain = account.domain, + let updatedAccount = try? await context.apiService.fetchUser(username: account.acct, domain: domain, authenticationBox: authContext.mastodonAuthenticationBox) { + viewModel.account = updatedAccount - if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { - viewModel.me = updatedMe - FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) + viewModel.relationship = relationship + self.profileHeaderViewController.viewModel.relationship = relationship + self.profileHeaderViewController.profileHeaderView.viewModel.relationship = relationship + } + + viewModel.isUpdating = false + } + } else if viewModel.account == viewModel.me { + // update my profile + Task { + if let updatedMe = try? await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value { + viewModel.me = updatedMe + viewModel.account = updatedMe + FileManager.default.store(account: updatedMe, forUserID: authContext.mastodonAuthenticationBox.authentication.userIdentifier()) + } + + viewModel.isUpdating = false } - viewModel.relationship = relationship - viewModel.isUpdating = false } } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 657296009..a0c4a9bd1 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -74,16 +74,7 @@ extension UserTableViewCellDelegate where Self: NeedsDependency & AuthContextPro await MainActor.run { view.viewModel.relationship = relationship view.updateButtonState(with: relationship, isMe: isMe) - - if let relationship { - let userInfo = [ - "relationship": relationship, - ] - - NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) - } } - } } } From 0e46a811b79db3aaed4cb7578666777ab09ead2f Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 19 Feb 2024 10:18:12 +0100 Subject: [PATCH 149/159] Use constant foe UserInfo-key instead of string (IOS-192) --- Mastodon/Protocol/Provider/DataSourceFacade+Block.swift | 2 +- Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift | 6 +++--- Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift | 2 +- Mastodon/Scene/Profile/ProfileViewController.swift | 4 ++-- .../Service/API/APIService+HomeTimeline.swift | 3 +-- .../Service/API/APIService+Relationship.swift | 8 ++++++++ 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index 02bf7893b..139de62dc 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -27,7 +27,7 @@ extension DataSourceFacade { ) let userInfo = [ - "relationship": response.value, + .relationship: response.value, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 2c16c8241..b47aa9e76 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -27,7 +27,7 @@ extension DataSourceFacade { dependency.context.authenticationService.fetchFollowingAndBlockedAsync() NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ - "relationship": response + .relationship: response ]) return response @@ -75,7 +75,7 @@ extension DataSourceFacade { } NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ - "relationship": newRelationship + .relationship: newRelationship ]) await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox) @@ -115,7 +115,7 @@ extension DataSourceFacade { ) let userInfo = [ - "relationship": newRelationship, + .relationship: newRelationship, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 3e70d7b09..b072e8e27 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -23,7 +23,7 @@ extension DataSourceFacade { ) let userInfo = [ - "relationship": response.value, + .relationship: response.value, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 47c93d3c1..09ee2235d 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -833,7 +833,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { // we need to trigger this here as domain block doesn't return a relationship let userInfo = [ - "relationship": newRelationship, + .relationship: newRelationship, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) @@ -1002,7 +1002,7 @@ extension ProfileViewController { @objc func relationshipChanged(_ notification: Notification) { - guard let userInfo = notification.userInfo, let relationship = userInfo["relationship"] as? Mastodon.Entity.Relationship else { + guard let userInfo = notification.userInfo, let relationship = userInfo[.relationship] as? Mastodon.Entity.Relationship else { return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 976ccfb73..17544b4fe 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -11,9 +11,8 @@ import CoreData import CoreDataStack import MastodonSDK -public extension Foundation.Notification.Name { +public extension Notification.Name { static let userFetched = Notification.Name(rawValue: "org.joinmastodon.app.user-fetched") - static let relationshipChanged = Notification.Name(rawValue: "org.joinmastodon.app.relationship-changed") } extension APIService { diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index ce366ad7e..69b378cb2 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -11,6 +11,14 @@ import CoreData import CoreDataStack import MastodonSDK +extension Notification.Name { + public static let relationshipChanged = Notification.Name(rawValue: "org.joinmastodon.app.relationship-changed") +} + +extension AnyHashable { + public static let relationship = "relationship" +} + extension APIService { public func relationship( forAccounts accounts: [Mastodon.Entity.Account], From ed7de63eb766edffac8bfb2f7b8b1adf317900ec Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 19 Feb 2024 11:52:12 +0100 Subject: [PATCH 150/159] Remove unused code (IOS-192) --- Mastodon/Scene/Root/Tab.swift | 40 ----------------------------------- 1 file changed, 40 deletions(-) diff --git a/Mastodon/Scene/Root/Tab.swift b/Mastodon/Scene/Root/Tab.swift index a3303ba4e..dca5e1331 100644 --- a/Mastodon/Scene/Root/Tab.swift +++ b/Mastodon/Scene/Root/Tab.swift @@ -55,46 +55,6 @@ enum Tab: Int, CaseIterable { var largeImage: UIImage { return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80)) } - -// @MainActor -// func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController { -// guard let authContext else { return UITableViewController() } -// -// let viewController: UIViewController -// switch self { -// case .home: -// let _viewController = HomeTimelineViewController() -// _viewController.context = context -// _viewController.coordinator = coordinator -// _viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext) -// viewController = _viewController -// case .search: -// let _viewController = SearchViewController() -// _viewController.context = context -// _viewController.coordinator = coordinator -// _viewController.viewModel = SearchViewModel(context: context, authContext: authContext) -// viewController = _viewController -// case .compose: -// viewController = UIViewController() -// case .notifications: -// let _viewController = NotificationViewController() -// _viewController.context = context -// _viewController.coordinator = coordinator -// _viewController.viewModel = NotificationViewModel(context: context, authContext: authContext) -// viewController = _viewController -// case .me: -// #warning("What happens if there's no me at the beginning? I guess we _do_ need another migration?") -// guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return UIViewController() } -// -// let _viewController = ProfileViewController() -// _viewController.context = context -// _viewController.coordinator = coordinator -// _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me) -// viewController = _viewController -// } -// viewController.title = self.title -// return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) -// } } extension UIViewController { From 52f5458364b268a3a6d0249c9747ff2709867d3b Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 19 Feb 2024 12:05:34 +0100 Subject: [PATCH 151/159] Fix build --- Mastodon/Protocol/Provider/DataSourceFacade+Block.swift | 2 +- Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift | 6 +++--- Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift | 2 +- .../Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift | 2 +- Mastodon/Scene/Profile/ProfileViewController.swift | 4 ++-- .../MastodonCore/Service/API/APIService+Relationship.swift | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index 139de62dc..b5d59ef67 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -27,7 +27,7 @@ extension DataSourceFacade { ) let userInfo = [ - .relationship: response.value, + UserInfoKey.relationship: response.value, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index b47aa9e76..b024cfa96 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -27,7 +27,7 @@ extension DataSourceFacade { dependency.context.authenticationService.fetchFollowingAndBlockedAsync() NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ - .relationship: response + UserInfoKey.relationship: response ]) return response @@ -75,7 +75,7 @@ extension DataSourceFacade { } NotificationCenter.default.post(name: .relationshipChanged, object: nil, userInfo: [ - .relationship: newRelationship + UserInfoKey.relationship: newRelationship ]) await notificationView.configure(notification: notification, authenticationBox: dependency.authContext.mastodonAuthenticationBox) @@ -115,7 +115,7 @@ extension DataSourceFacade { ) let userInfo = [ - .relationship: newRelationship, + UserInfoKey.relationship: newRelationship, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index b072e8e27..11a975381 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -23,7 +23,7 @@ extension DataSourceFacade { ) let userInfo = [ - .relationship: response.value, + UserInfoKey.relationship: response.value, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) diff --git a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift index e7bf776c7..b67ac9b5d 100644 --- a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift @@ -57,7 +57,7 @@ extension ProfileCardView.ViewModel { private func bindHeader(view: ProfileCardView) { $authorBannerImageURL .sink { url in - guard let url = url, !url.absoluteString.hasSuffix("missing.png") else { + guard let url = url, !url.absoluteString.hasSuffix(Mastodon.Entity.Account.missingImageName) else { view.bannerImageView.image = .placeholder(color: .systemGray3) return } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 09ee2235d..6141014a9 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -833,7 +833,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { // we need to trigger this here as domain block doesn't return a relationship let userInfo = [ - .relationship: newRelationship, + UserInfoKey.relationship: newRelationship, ] NotificationCenter.default.post(name: .relationshipChanged, object: self, userInfo: userInfo) @@ -1002,7 +1002,7 @@ extension ProfileViewController { @objc func relationshipChanged(_ notification: Notification) { - guard let userInfo = notification.userInfo, let relationship = userInfo[.relationship] as? Mastodon.Entity.Relationship else { + guard let userInfo = notification.userInfo, let relationship = userInfo[UserInfoKey.relationship] as? Mastodon.Entity.Relationship else { return } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index 69b378cb2..4336aa0a1 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -15,7 +15,7 @@ extension Notification.Name { public static let relationshipChanged = Notification.Name(rawValue: "org.joinmastodon.app.relationship-changed") } -extension AnyHashable { +public enum UserInfoKey { public static let relationship = "relationship" } From 49af28eebad2dad982bea25ecbf121d9726807e0 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 19 Feb 2024 12:06:10 +0100 Subject: [PATCH 152/159] Use constant (IOS-192) --- Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift | 3 ++- .../Scene/Profile/Header/ProfileHeaderViewController.swift | 1 - .../MastodonSDK/Entity/Mastodon+Entity+Account.swift | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index 9a3f5ed90..334dc703f 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -11,6 +11,7 @@ import CoreData import CoreDataStack import Pageboy import MastodonCore +import MastodonSDK protocol MediaPreviewPage: UIViewController { func setShowingChrome(_ showingChrome: Bool) @@ -152,7 +153,7 @@ extension MediaPreviewViewModel { case .profileBanner(let item): guard let assertURL = item.assetURL else { return false } - return assertURL.hasSuffix("missing.png") == false + return assertURL.hasSuffix(Mastodon.Entity.Account.missingImageName) == false } } } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 4c8949e16..81f723402 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -105,7 +105,6 @@ final class ProfileHeaderViewController: UIViewController, NeedsDependency, Medi override func viewDidLoad() { super.viewDidLoad() - view.setContentHuggingPriority(.required - 1, for: .vertical) view.backgroundColor = .systemBackground diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index be4225659..29b73763c 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -144,7 +144,7 @@ extension Mastodon.Entity.Account { } public func avatarImageURLWithFallback(domain: String) -> URL { - return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! + return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/\(Self.missingImageName)")! } public var displayNameWithFallback: String { @@ -163,3 +163,7 @@ extension Mastodon.Entity.Account { } } + +extension Mastodon.Entity.Account { + public static let missingImageName = "missing.png" +} From 22b9e912f6b14809eb20a10b85af535b532d4157 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Mon, 19 Feb 2024 12:09:51 +0100 Subject: [PATCH 153/159] Remove unused completion (IOS-192) --- .../Sources/MastodonCore/AuthenticationServiceProvider.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift index f5a9c5ea8..544d3836d 100644 --- a/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift +++ b/MastodonSDK/Sources/MastodonCore/AuthenticationServiceProvider.swift @@ -106,7 +106,7 @@ public extension AuthenticationServiceProvider { userDefaults.didMigrateAuthentications == false } - func fetchAccounts(apiService: APIService, completion: (() -> Void)? = nil) async { + func fetchAccounts(apiService: APIService) async { // FIXME: This is a dirty hack to make the performance-stuff work. // Problem is, that we don't persist the user on disk anymore. So we have to fetch // it when we need it to display on the home timeline. @@ -120,10 +120,7 @@ public extension AuthenticationServiceProvider { } NotificationCenter.default.post(name: .userFetched, object: nil) - - completion?() } - } // MARK: - Private From 897fc0d3481008426e6436f569507a4c3224f400 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 20 Feb 2024 12:04:44 +0100 Subject: [PATCH 154/159] Recude task-nestedness (IOS-192) --- Mastodon/Coordinator/SceneCoordinator.swift | 139 ++++++++++---------- 1 file changed, 66 insertions(+), 73 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index f2c7d4c6f..997a15aeb 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -46,13 +46,13 @@ final public class SceneCoordinator { self.appContext = appContext scene.session.sceneCoordinator = self - + appContext.notificationService.requestRevealNotificationPublisher .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] pushNotification in - guard let self = self else { return } - Task { + guard let self else { return } + Task { @MainActor in guard let currentActiveAuthenticationBox = self.authContext?.mastodonAuthenticationBox else { return } let accessToken = pushNotification.accessToken // use raw accessToken value without normalize if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken { @@ -68,82 +68,75 @@ final public class SceneCoordinator { let userID = authentication.userID let isSuccess = try await appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID) guard isSuccess else { return } - + self.setup() try await Task.sleep(nanoseconds: .second * 1) - + // redirect to notifications tab self.switchToTabBar(tab: .notifications) - - // Delay in next run loop - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Note: - // show (push) on phone and pad - let from: UIViewController? = { - if let splitViewController = self.splitViewController { - if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil { - // compact - return splitViewController.compactMainTabBarViewController.topMost - } else { - // expand - return splitViewController.contentSplitViewController.mainTabBarController.topMost - } + + // Note: + // show (push) on phone and pad + let from: UIViewController? = { + if let splitViewController = self.splitViewController { + if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil { + // compact + return splitViewController.compactMainTabBarViewController.topMost } else { - return self.tabBarController.topMost + // expand + return splitViewController.contentSplitViewController.mainTabBarController.topMost } - }() - - // show notification related content - guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } - guard let authContext = self.authContext else { return } - guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return } - let notificationID = String(pushNotification.notificationID) - - switch type { - case .follow: - Task { - let account = try await appContext.apiService.notification( - notificationID: notificationID, - authenticationBox: authContext.mastodonAuthenticationBox - ).value.account - - let relationship = try await appContext.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first - - let profileViewModel = ProfileViewModel( - context: appContext, - authContext: authContext, - account: account, - relationship: relationship, - me: me - ) - _ = self.present( - scene: .profile(viewModel: profileViewModel), - from: from, - transition: .show - ) - } - case .followRequest: - // do nothing - break - case .mention, .reblog, .favourite, .poll, .status: - let threadViewModel = RemoteThreadViewModel( - context: appContext, - authContext: authContext, - notificationID: notificationID - ) - _ = self.present( - scene: .thread(viewModel: threadViewModel), - from: from, - transition: .show - ) - - case ._other: - assertionFailure() - break + } else { + return self.tabBarController.topMost } - } // end DispatchQueue.main.async + }() + + // show notification related content + guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } + guard let authContext = self.authContext else { return } + guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return } + let notificationID = String(pushNotification.notificationID) + + switch type { + case .follow: + let account = try await appContext.apiService.notification( + notificationID: notificationID, + authenticationBox: authContext.mastodonAuthenticationBox + ).value.account + + let relationship = try await appContext.apiService.relationship(forAccounts: [account], authenticationBox: authContext.mastodonAuthenticationBox).value.first + + let profileViewModel = ProfileViewModel( + context: appContext, + authContext: authContext, + account: account, + relationship: relationship, + me: me + ) + _ = self.present( + scene: .profile(viewModel: profileViewModel), + from: from, + transition: .show + ) + case .followRequest: + // do nothing + break + case .mention, .reblog, .favourite, .poll, .status: + let threadViewModel = RemoteThreadViewModel( + context: appContext, + authContext: authContext, + notificationID: notificationID + ) + _ = self.present( + scene: .thread(viewModel: threadViewModel), + from: from, + transition: .show + ) + + case ._other: + assertionFailure() + break + } } catch { assertionFailure(error.localizedDescription) @@ -170,7 +163,7 @@ extension SceneCoordinator { case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil) case none } - + enum Scene { // onboarding case welcome From 86e968fabd725b189def1e9e51f0c937fa604c66 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 20 Feb 2024 15:26:05 +0100 Subject: [PATCH 155/159] Don't reload content all the time (IOS-192) --- .../Header/View/ProfileHeaderView+ViewModel.swift | 6 ------ Mastodon/Scene/Profile/ProfileViewModel.swift | 12 +++++------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index a292e75ea..809eb9a4c 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -56,12 +56,6 @@ extension ProfileHeaderView { self.account = account self.me = me self.relationship = relationship - - #warning("TODO: Implement") -// $relationshipActionOptionSet -// .compactMap { $0.highPriorityAction(except: []) } -// .map { $0 == .none } -// .assign(to: &$isRelationshipActionButtonHidden) } } } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 9a8b567ae..76969dedc 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -84,13 +84,11 @@ class ProfileViewModel: NSObject { self.profileAboutViewModel = ProfileAboutViewModel(context: context, account: account) super.init() - // bind user - $account - .map { user -> UserIdentifier? in - guard let domain = account.domain else { return nil } - return MastodonUserIdentifier(domain: domain, userID: account.id) - } - .assign(to: &$userIdentifier) + if let domain = account.domain { + userIdentifier = MastodonUserIdentifier(domain: domain, userID: account.id) + } else { + userIdentifier = nil + } // bind userIdentifier $userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier) From d7fa94211aab72279271caf4dbeeabcf4f745d60 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 20 Feb 2024 16:28:23 +0100 Subject: [PATCH 156/159] Fix warning --- Mastodon/Scene/Profile/ProfileViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 6141014a9..ddd28307e 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -803,7 +803,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in guard let self else { return } Task { - let newRelationship = try await DataSourceFacade.responseToUserBlockAction( + _ = try await DataSourceFacade.responseToUserBlockAction( dependency: self, account: account ) From 8630bd17462ee07e07a773265ddceb0e1134861c Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 20 Feb 2024 16:33:27 +0100 Subject: [PATCH 157/159] Show loading-indicator (IOS-192) --- .../Provider/DataSourceFacade+Profile.swift | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index ec91d9c82..89fd2547a 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -101,8 +101,14 @@ extension DataSourceFacade { provider: ViewControllerWithDependencies & AuthContextProvider, account: Mastodon.Entity.Account ) async { + provider.coordinator.showLoading() + guard let me = provider.authContext.mastodonAuthenticationBox.authentication.account(), - let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else { return } + let relationship = try? await provider.context.apiService.relationship(forAccounts: [account], authenticationBox: provider.authContext.mastodonAuthenticationBox).value.first else { + return provider.coordinator.hideLoading() + } + + provider.coordinator.hideLoading() let profileViewModel = ProfileViewModel( context: provider.context, @@ -155,17 +161,6 @@ extension DataSourceFacade { } extension DataSourceFacade { - - struct ProfileActionMenuContext { - let isMuting: Bool - let isBlocking: Bool - let isMyself: Bool - - let cell: UITableViewCell? - let sourceView: UIView? - let barButtonItem: UIBarButtonItem? - } - static func createActivityViewController( dependency: NeedsDependency, account: Mastodon.Entity.Account From a170a462ee837d0337315e8f0a5518f11fa78d29 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 20 Feb 2024 16:47:55 +0100 Subject: [PATCH 158/159] Remove nested tasks (IOS-192) --- .../Provider/DataSourceFacade+Status.swift | 72 +++++++++---------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index dbfc931ba..63d52a515 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -235,25 +235,21 @@ extension DataSourceFacade { alertController.addAction(cancelAction) dependency.present(alertController, animated: true) case .reportUser: - Task { + guard let relationship = try? await dependency.context.apiService.relationship(forAccounts: [menuContext.author], authenticationBox: dependency.authContext.mastodonAuthenticationBox).value.first else { return } - guard let relationship = try? await dependency.context.apiService.relationship(forAccounts: [menuContext.author], authenticationBox: dependency.authContext.mastodonAuthenticationBox).value.first else { return } + let reportViewModel = ReportViewModel( + context: dependency.context, + authContext: dependency.authContext, + account: menuContext.author, + relationship: relationship, + status: menuContext.statusViewModel?.originalStatus + ) - let reportViewModel = ReportViewModel( - context: dependency.context, - authContext: dependency.authContext, - account: menuContext.author, - relationship: relationship, - status: menuContext.statusViewModel?.originalStatus - ) - - _ = dependency.coordinator.present( - scene: .report(viewModel: reportViewModel), - from: dependency, - transition: .modal(animated: true, completion: nil) - ) - } // end Task - + _ = dependency.coordinator.present( + scene: .report(viewModel: reportViewModel), + from: dependency, + transition: .modal(animated: true, completion: nil) + ) case .shareUser: let activityViewController = DataSourceFacade.createActivityViewController( dependency: dependency, @@ -270,7 +266,6 @@ extension DataSourceFacade { transition: .activityViewControllerPresent(animated: true, completion: nil) ) case .bookmarkStatus: - Task { guard let status = menuContext.statusViewModel?.originalStatus else { assertionFailure() return @@ -279,29 +274,26 @@ extension DataSourceFacade { provider: dependency, status: status ) - } // end Task case .shareStatus: - Task { - guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else { - assertionFailure() - return - } + guard let status: MastodonStatus = menuContext.statusViewModel?.originalStatus?.reblog ?? menuContext.statusViewModel?.originalStatus else { + assertionFailure() + return + } - let activityViewController = try await DataSourceFacade.createActivityViewController( - dependency: dependency, - status: status - ) - - _ = dependency.coordinator.present( - scene: .activityViewController( - activityViewController: activityViewController, - sourceView: menuContext.button, - barButtonItem: menuContext.barButtonItem - ), - from: dependency, - transition: .activityViewControllerPresent(animated: true, completion: nil) - ) - } // end Task + let activityViewController = try await DataSourceFacade.createActivityViewController( + dependency: dependency, + status: status + ) + + _ = dependency.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: menuContext.button, + barButtonItem: menuContext.barButtonItem + ), + from: dependency, + transition: .activityViewControllerPresent(animated: true, completion: nil) + ) case .deleteStatus: let alertController = UIAlertController( title: L10n.Common.Alerts.DeletePost.title, @@ -358,7 +350,7 @@ extension DataSourceFacade { // do nothing, as the translation is reverted in `StatusTableViewCellDelegate` in `DataSourceProvider+StatusTableViewCellDelegate.swift`. break case .followUser(_): - try await DataSourceFacade.responseToUserFollowAction(dependency: dependency, + _ = try await DataSourceFacade.responseToUserFollowAction(dependency: dependency, account: menuContext.author) case .blockDomain(let context): let title: String From f6e630aa6639c31f888a06a977a128f7a9f35936 Mon Sep 17 00:00:00 2001 From: Nathan Mattes Date: Tue, 20 Feb 2024 17:41:10 +0100 Subject: [PATCH 159/159] Cleanup (IOS-192) --- .../ForYou/ProfileCardView+ViewModel.swift | 2 +- .../Posts/DiscoveryPostsViewModel.swift | 1 - ...ineViewController+DataSourceProvider.swift | 1 - .../NotificationTimelineViewModel.swift | 1 - .../Scene/Profile/ProfileViewController.swift | 1 - .../Root/MainTab/MainTabBarController.swift | 22 +-- .../Scene/Root/Sidebar/SidebarViewModel.swift | 12 +- Mastodon/Supporting Files/SceneDelegate.swift | 138 +++++++++--------- .../Persistence/Persistence.swift | 2 +- .../Notification/NotificationService.swift | 49 +++---- 10 files changed, 111 insertions(+), 118 deletions(-) diff --git a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift index b67ac9b5d..05bd54fca 100644 --- a/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/ProfileCardView+ViewModel.swift @@ -57,7 +57,7 @@ extension ProfileCardView.ViewModel { private func bindHeader(view: ProfileCardView) { $authorBannerImageURL .sink { url in - guard let url = url, !url.absoluteString.hasSuffix(Mastodon.Entity.Account.missingImageName) else { + guard let url, !url.absoluteString.hasSuffix(Mastodon.Entity.Account.missingImageName) else { view.bannerImageView.image = .placeholder(color: .systemGray3) return } diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift index 15b5064b0..47f695987 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift @@ -46,7 +46,6 @@ final class DiscoveryPostsViewModel { self.context = context self.authContext = authContext self.dataController = StatusDataController() - // end init Task { await checkServerEndpoint() diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index c6de4b6ad..e4bcc8a12 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -25,7 +25,6 @@ extension NotificationTimelineViewController: DataSourceProvider { let item: DataSourceItem? = { guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil } - //TODO: Get relationship if let notification = feed.notification { let mastodonNotification = MastodonNotification.fromEntity(notification, relationship: nil) return .notification(record: mastodonNotification) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index e6a48d092..c2ac144f8 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -56,7 +56,6 @@ final class NotificationTimelineViewModel { switch scope { case .everything: - //TODO: I need the relationship here, too self.dataController.records = (try? FileManager.default.cachedNotificationsAll(for: authContext.mastodonAuthenticationBox))?.map({ notification in MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationAll) }) ?? [] diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index ddd28307e..35702c0d8 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -409,7 +409,6 @@ extension ProfileViewController { guard let relationship else { return } - // they don't run as there's not a change, probably? self.viewModel.relationship = relationship self.viewModel.account = account } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index b32f96000..5cd0d9d7c 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -127,17 +127,17 @@ extension MainTabBarController { .sink { [weak self] error in guard let self, let coordinator = self.coordinator else { return } switch error { - case .implicit: - break - case .explicit: - let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) - let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) - alertController.addAction(okAction) - _ = coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) + case .implicit: + break + case .explicit: + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alertController.addAction(okAction) + _ = coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) } } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index 85c47fc42..c3aa06892 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -74,12 +74,12 @@ extension SidebarViewModel { let imageURL: URL? switch item { - case .me: - let account = self.authContext?.mastodonAuthenticationBox.authentication.account() - imageURL = account?.avatarImageURL() - case .home, .search, .compose, .notifications: - // no custom avatar for other tabs - imageURL = nil + case .me: + let account = self.authContext?.mastodonAuthenticationBox.authentication.account() + imageURL = account?.avatarImageURL() + case .home, .search, .compose, .notifications: + // no custom avatar for other tabs + imageURL = nil } cell.item = SidebarListContentView.Item( diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 014c0cedb..75aa27c6e 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -267,77 +267,77 @@ extension SceneDelegate { print("source application = \(sendingAppID ?? "Unknown")") print("url = \(url)") #endif - + switch url.host { - case "post": - showComposeViewController() - case "profile": - let components = url.pathComponents - guard - components.count == 2, - components[0] == "/", - let authContext = coordinator?.authContext - else { return } - - Task { - do { - let authenticationBox = authContext.mastodonAuthenticationBox - - guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return } - - guard let account = try await AppContext.shared.apiService.search( - query: .init(q: components[1], type: .accounts, resolve: true), - authenticationBox: authenticationBox - ).value.accounts.first else { return } - - guard let relationship = try await AppContext.shared.apiService.relationship( - forAccounts: [account], - authenticationBox: authenticationBox - ).value.first else { return } - - let profileViewModel = ProfileViewModel( - context: AppContext.shared, - authContext: authContext, - account: account, - relationship: relationship, - me: me - ) - - self.coordinator?.present( - scene: .profile(viewModel: profileViewModel), - from: nil, - transition: .show - ) - } catch { - // fail silently - } + case "post": + showComposeViewController() + case "profile": + let components = url.pathComponents + guard + components.count == 2, + components[0] == "/", + let authContext = coordinator?.authContext + else { return } + + Task { + do { + let authenticationBox = authContext.mastodonAuthenticationBox + + guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return } + + guard let account = try await AppContext.shared.apiService.search( + query: .init(q: components[1], type: .accounts, resolve: true), + authenticationBox: authenticationBox + ).value.accounts.first else { return } + + guard let relationship = try await AppContext.shared.apiService.relationship( + forAccounts: [account], + authenticationBox: authenticationBox + ).value.first else { return } + + let profileViewModel = ProfileViewModel( + context: AppContext.shared, + authContext: authContext, + account: account, + relationship: relationship, + me: me + ) + + self.coordinator?.present( + scene: .profile(viewModel: profileViewModel), + from: nil, + transition: .show + ) + } catch { + // fail silently } - case "status": - let components = url.pathComponents - guard - components.count == 2, - components[0] == "/", - let authContext = coordinator?.authContext - else { return } - let statusId = components[1] - // View post from user - let threadViewModel = RemoteThreadViewModel( - context: AppContext.shared, - authContext: authContext, - statusID: statusId - ) - coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - case "search": - let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems - guard - let authContext = coordinator?.authContext, - let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value - else { return } - - let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery) - coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show) - default: - return + } + case "status": + let components = url.pathComponents + guard + components.count == 2, + components[0] == "/", + let authContext = coordinator?.authContext + else { return } + let statusId = components[1] + // View post from user + let threadViewModel = RemoteThreadViewModel( + context: AppContext.shared, + authContext: authContext, + statusID: statusId + ) + coordinator?.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + case "search": + let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems + guard + let authContext = coordinator?.authContext, + let searchQuery = queryItems?.first(where: { $0.name == "query" })?.value + else { return } + + let viewModel = SearchDetailViewModel(authContext: authContext, initialSearchText: searchQuery) + coordinator?.present(scene: .searchDetail(viewModel: viewModel), from: nil, transition: .show) + default: + return } } } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift index b933ca2ae..48ce11f07 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence.swift @@ -22,7 +22,7 @@ public enum Persistence { private var filename: String { switch self { case .searchHistory(let userIdentifier): - return "search_history_\(uniqueUserDomainIdentifier(for: userIdentifier))" // todo: @zeitschlag should this be user-scoped as well? + return "search_history_\(uniqueUserDomainIdentifier(for: userIdentifier))" case let .homeTimeline(userIdentifier): return "home_timeline_\(uniqueUserDomainIdentifier(for: userIdentifier))" case let .notificationsMentions(userIdentifier): diff --git a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift index b69b57ded..727cb7d18 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/Notification/NotificationService.swift @@ -97,33 +97,30 @@ extension NotificationService { extension NotificationService { public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] { guard let authenticationService = self.authenticationService else { return [] } - let managedObjectContext = authenticationService.managedObjectContext - return try await managedObjectContext.perform { - var items: [UIApplicationShortcutItem] = [] - for authentication in AuthenticationServiceProvider.shared.authentications { - guard let account = authentication.account() else { continue } - let accessToken = authentication.userAccessToken - let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) - guard count > 0 else { continue } - - let title = "@\(account.acctWithDomain)" - let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) - - let item = UIApplicationShortcutItem( - type: NotificationService.unreadShortcutItemIdentifier, - localizedTitle: title, - localizedSubtitle: subtitle, - icon: nil, - userInfo: [ - "accessToken": accessToken as NSSecureCoding - ] - ) - items.append(item) - } - return items + + var items: [UIApplicationShortcutItem] = [] + for authentication in AuthenticationServiceProvider.shared.authentications { + guard let account = authentication.account() else { continue } + let accessToken = authentication.userAccessToken + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + guard count > 0 else { continue } + + let title = "@\(account.acctWithDomain)" + let subtitle = L10n.A11y.Plural.Count.Unread.notification(count) + + let item = UIApplicationShortcutItem( + type: NotificationService.unreadShortcutItemIdentifier, + localizedTitle: title, + localizedSubtitle: subtitle, + icon: nil, + userInfo: [ + "accessToken": accessToken as NSSecureCoding + ] + ) + items.append(item) } - } -} + return items + }} extension NotificationService {