From 36091e96289d57b20b383a82d995fa76a8b8fc57 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 16 Nov 2023 16:54:25 +0100 Subject: [PATCH] Begin removing MastodonStatus, MastodonUser and related from CoreData (IOS-176, IOS-189) --- Mastodon.xcodeproj/project.pbxproj | 8 - .../Diffable/Discovery/DiscoveryItem.swift | 3 +- .../Diffable/Discovery/DiscoverySection.swift | 33 +- .../Notification/NotificationItem.swift | 7 +- .../Notification/NotificationSection.swift | 17 +- Mastodon/Diffable/Report/ReportItem.swift | 6 +- Mastodon/Diffable/Status/StatusItem.swift | 14 +- Mastodon/Diffable/Status/StatusSection.swift | 137 ++--- Mastodon/Diffable/User/UserItem.swift | 2 +- Mastodon/Diffable/User/UserSection.swift | 10 +- .../Provider/DataSourceFacade+Bookmark.swift | 5 +- .../Provider/DataSourceFacade+Favorite.swift | 5 +- .../Provider/DataSourceFacade+Follow.swift | 145 ++--- .../Provider/DataSourceFacade+Hashtag.swift | 2 - .../Provider/DataSourceFacade+Media.swift | 8 +- .../Provider/DataSourceFacade+Meta.swift | 9 +- .../Provider/DataSourceFacade+Model.swift | 46 +- .../Provider/DataSourceFacade+Mute.swift | 4 +- .../Provider/DataSourceFacade+Profile.swift | 48 +- .../Provider/DataSourceFacade+Reblog.swift | 4 +- .../DataSourceFacade+SearchHistory.swift | 99 ++- .../DataSourceFacade+Status+History.swift | 2 +- .../Provider/DataSourceFacade+Status.swift | 97 ++- .../Provider/DataSourceFacade+Thread.swift | 10 +- .../Provider/DataSourceFacade+Translate.swift | 16 +- .../Provider/DataSourceFacade+URL.swift | 3 +- .../Provider/DataSourceFacade+UserView.swift | 59 -- ...er+NotificationTableViewCellDelegate.swift | 108 +--- ...Provider+StatusTableViewCellDelegate.swift | 174 ++---- ...tatusTableViewControllerNavigateable.swift | 14 +- ...taSourceProvider+UITableViewDelegate.swift | 23 +- .../Provider/DataSourceProvider.swift | 9 +- Mastodon/Scene/Compose/ComposeViewModel.swift | 2 +- ...DiscoveryCommunityViewModel+Diffable.swift | 2 +- .../DiscoveryCommunityViewModel+State.swift | 8 +- .../DiscoveryCommunityViewModel.swift | 8 +- .../DiscoveryForYouViewController.swift | 8 +- .../DiscoveryForYouViewModel+Diffable.swift | 2 +- .../ForYou/DiscoveryForYouViewModel.swift | 17 +- .../DiscoveryPostsViewModel+Diffable.swift | 2 +- .../Posts/DiscoveryPostsViewModel+State.swift | 8 +- .../Posts/DiscoveryPostsViewModel.swift | 8 +- .../HashtagTimelineViewModel+Diffable.swift | 2 +- .../HashtagTimelineViewModel+State.swift | 12 +- .../HashtagTimelineViewModel.swift | 30 +- ...ineViewController+DataSourceProvider.swift | 16 +- .../HomeTimelineViewModel+Diffable.swift | 66 +- ...omeTimelineViewModel+LoadLatestState.swift | 13 +- ...omeTimelineViewModel+LoadOldestState.swift | 12 +- .../HomeTimeline/HomeTimelineViewModel.swift | 85 +-- .../NotificationTableViewCell+ViewModel.swift | 4 +- ...ineViewController+DataSourceProvider.swift | 11 +- .../NotificationTimelineViewController.swift | 7 +- ...tificationTimelineViewModel+Diffable.swift | 65 +- ...ionTimelineViewModel+LoadOldestState.swift | 12 +- .../NotificationTimelineViewModel.swift | 118 ++-- .../Bookmark/BookmarkViewModel+Diffable.swift | 2 +- .../Bookmark/BookmarkViewModel+State.swift | 10 +- .../Profile/Bookmark/BookmarkViewModel.swift | 14 +- .../Profile/CachedProfileViewModel.swift | 17 - .../FamiliarFollowersViewModel+Diffable.swift | 2 +- .../FamiliarFollowersViewModel.swift | 20 +- .../Favorite/FavoriteViewModel+Diffable.swift | 2 +- .../Favorite/FavoriteViewModel+State.swift | 10 +- .../Profile/Favorite/FavoriteViewModel.swift | 12 +- .../FollowerListViewModel+Diffable.swift | 2 +- .../FollowerListViewModel+State.swift | 10 +- .../Follower/FollowerListViewModel.swift | 13 +- .../Scene/Profile/MeProfileViewModel.swift | 15 +- Mastodon/Scene/Profile/ProfileViewModel.swift | 28 +- .../Profile/RemoteProfileViewModel.swift | 47 +- .../UserTimelineViewModel+Diffable.swift | 2 +- .../UserTimelineViewModel+State.swift | 12 +- .../Timeline/UserTimelineViewModel.swift | 15 +- .../UserLIst/UserListViewModel+Diffable.swift | 2 +- .../UserLIst/UserListViewModel+State.swift | 13 +- .../Profile/UserLIst/UserListViewModel.swift | 21 +- .../Scene/Report/Report/ReportViewModel.swift | 121 ++-- .../ReportResult/ReportResultViewModel.swift | 10 +- .../ReportStatusViewModel+Diffable.swift | 2 +- .../ReportStatusViewModel+State.swift | 18 +- .../ReportStatus/ReportStatusViewModel.swift | 27 +- .../ReportSupplementaryViewModel.swift | 5 +- .../ReportStatusTableViewCell+ViewModel.swift | 6 +- .../SearchResultOverviewCoordinator.swift | 37 +- .../SearchHistory/SearchHistoryItem.swift | 7 +- ...oryViewController+DataSourceProvider.swift | 2 +- .../SearchHistoryViewModel+Diffable.swift | 35 +- .../SearchHistoryViewModel.swift | 13 +- .../SearchResult/SearchResultItem.swift | 6 +- .../SearchResultViewModel+Diffable.swift | 4 +- .../SearchResult/SearchResultViewModel.swift | 29 +- .../NotificationView+Configuration.swift | 244 +++----- .../PollOptionView+Configuration.swift | 132 ++-- .../View/Content/UserView+Configuration.swift | 42 +- .../StatusTableViewCell+ViewModel.swift | 15 +- ...tusThreadRootTableViewCell+ViewModel.swift | 4 +- .../UserTableViewCell+ViewModel.swift | 76 +-- .../TableviewCell/UserTableViewCell.swift | 1 - .../RecommendAccountItem.swift | 4 +- .../RecommendAccountSection.swift | 17 +- .../SuggestionAccountViewController.swift | 10 +- .../SuggestionAccountViewModel.swift | 38 +- ...estionAccountTableViewCell+ViewModel.swift | 6 +- .../SuggestionAccountTableViewCell.swift | 4 +- .../Scene/Thread/CachedThreadViewModel.swift | 21 - .../StatusEditHistoryTableViewCell.swift | 2 +- .../StatusEditHistoryViewModel.swift | 3 +- .../Scene/Thread/RemoteThreadViewModel.swift | 24 +- .../Thread/ThreadViewModel+Diffable.swift | 8 +- Mastodon/Scene/Thread/ThreadViewModel.swift | 39 +- .../CoreDataStack/Entity/App/Feed.swift | 24 +- .../Entity/Mastodon/Application.swift | 2 +- .../CoreDataStack/Entity/Mastodon/Card.swift | 6 +- .../CoreDataStack/Entity/Mastodon/Emoji.swift | 2 +- .../Entity/Mastodon/MastodonUser.swift | 12 +- .../Entity/Mastodon/Notification.swift | 119 ++-- .../CoreDataStack/Entity/Mastodon/Poll.swift | 2 +- .../Entity/Mastodon/SearchHistory.swift | 6 +- .../Entity/Mastodon/Status.swift | 137 +++-- .../MastodonFollowRequestState.swift | 19 - .../Transient/MastodonNotificationType.swift | 2 +- .../MastodonAuthenticationBox.swift | 19 + .../CoreDataStack/MastodonStatus.swift | 2 +- .../CoreDataStack/Status+Property.swift | 58 +- .../Extension/CoreDataStack/Status.swift | 8 +- .../FeedFetchedResultsController.swift | 184 +++--- .../StatusFetchedResultsController.swift | 200 +++--- .../UserFetchedResultsController.swift | 216 +++---- .../Model/Compose/ComposeStatusItem.swift | 7 +- .../Model/MastodonStatusEntity.swift | 21 + .../MastodonCore/Model/Poll/PollItem.swift | 2 +- .../Persistence+Notification.swift | 63 +- .../Persistence/Persistence+Status.swift | 572 +++++++++--------- .../Service/API/APIService+Account.swift | 36 +- .../Service/API/APIService+Bookmark.swift | 191 +++--- .../Service/API/APIService+Favorite.swift | 248 ++++---- .../Service/API/APIService+Follow.swift | 58 +- .../API/APIService+HashtagTimeline.swift | 36 +- .../Service/API/APIService+HomeTimeline.swift | 130 ++-- .../Service/API/APIService+Mute.swift | 66 +- .../Service/API/APIService+Notification.swift | 141 +++-- .../Service/API/APIService+Poll.swift | 57 +- .../API/APIService+PublicTimeline.swift | 34 +- .../Service/API/APIService+Reblog.swift | 156 +++-- .../Service/API/APIService+Relationship.swift | 35 +- .../Service/API/APIService+Search.swift | 64 +- .../API/APIService+Status+History.swift | 6 +- .../API/APIService+Status+Publish.swift | 34 +- .../Service/API/APIService+Status.swift | 60 +- .../Service/API/APIService+Thread.swift | 38 +- .../Service/API/APIService+Trend.swift | 34 +- .../Service/API/APIService+UserTimeline.swift | 34 +- .../Entity/Mastodon+Entity+Account.swift | 43 ++ .../Entity/Mastodon+Entity+Card.swift | 10 + .../Entity/Mastodon+Entity+Notification.swift | 13 +- .../Entity/Mastodon+Entity+Poll.swift | 18 + .../Entity/Mastodon+Entity+Status.swift | 23 + .../Sources/MastodonSDK/FeedItem.swift | 21 + .../MastodonFollowRequestState.swift | 23 + .../MastodonNotificationType.swift | 41 ++ .../MastodonVisibility.swift | 8 +- .../Extension/MastodonVisibility+Image.swift | 2 +- .../Protocol/StatusCompatible.swift | 18 +- .../ComposeContentViewModel+DataSource.swift | 8 +- .../ComposeContentViewModel.swift | 55 +- .../MastodonStatusEditPublisher.swift | 4 +- .../Publisher/MastodonStatusPublisher.swift | 6 +- .../Content/MediaView+Configuration.swift | 8 +- .../Content/NotificationView+ViewModel.swift | 9 +- .../View/Content/NotificationView.swift | 2 - .../ProfileCardView+Configuration.swift | 69 +-- .../View/Content/StatusCardControl.swift | 23 +- .../Content/StatusView+Configuration.swift | 397 +++++------- .../View/Content/StatusView+ViewModel.swift | 6 +- .../MastodonUI/View/Content/StatusView.swift | 3 +- .../View/Content/UserView+ViewModel.swift | 3 +- .../MastodonUI/View/Content/UserView.swift | 5 +- ...ofileCardTableViewCell+Configuration.swift | 3 +- ...eMiddleLoaderTableViewCell+ViewModel.swift | 11 +- .../ViewModel/RelationshipViewModel.swift | 262 ++++---- 181 files changed, 3241 insertions(+), 3920 deletions(-) delete mode 100644 Mastodon/Scene/Profile/CachedProfileViewModel.swift delete mode 100644 Mastodon/Scene/Thread/CachedThreadViewModel.swift create mode 100644 MastodonSDK/Sources/MastodonCore/Model/MastodonStatusEntity.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/FeedItem.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/MastodonFollowRequestState.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/MastodonNotificationType.swift rename MastodonSDK/Sources/{CoreDataStack/Entity/Transient => MastodonSDK}/MastodonVisibility.swift (85%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 067137721..f76e235b5 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -388,7 +388,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 */; }; @@ -451,7 +450,6 @@ DBCBCBF4267CB070000F5B51 /* Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCBF3267CB070000F5B51 /* Decode85.swift */; }; DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */; }; DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B2F261440A50045B23D /* UITabBarController.swift */; }; - DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */; }; DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD376B1269302A4007FEC24 /* UITableViewCell.swift */; }; DBD5B1F827BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */; }; DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */; }; @@ -1102,7 +1100,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 = ""; }; @@ -1190,7 +1187,6 @@ DBCBCBF3267CB070000F5B51 /* Decode85.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Decode85.swift; sourceTree = ""; }; DBCBED1626132DB500B49291 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = ""; }; DBCC3B2F261440A50045B23D /* UITabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITabBarController.swift; sourceTree = ""; }; - DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedProfileViewModel.swift; sourceTree = ""; }; DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = ""; }; DBD5B1F727BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+TableViewControllerNavigateable.swift"; sourceTree = ""; }; DBD5B1F927BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+StatusTableViewControllerNavigateable.swift"; sourceTree = ""; }; @@ -2663,7 +2659,6 @@ DB938EEC2623F79B00E5B6C1 /* ThreadViewModel.swift */, DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */, DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */, - DB938F0226240EA300E5B6C1 /* CachedThreadViewModel.swift */, DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */, DB0FCB7F27968F70006C02E2 /* MastodonStatusThreadViewModel.swift */, ); @@ -2758,7 +2753,6 @@ DBFEEC97279BDC6A004F81DD /* About */, DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */, DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */, - DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */, DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */, DBB525632612C988002F1F29 /* MeProfileViewModel.swift */, ); @@ -3870,7 +3864,6 @@ 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, D8F9170D2A4B3C6F008A5370 /* AboutMastodonTableViewCell.swift in Sources */, DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */, - DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */, DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */, DB3EA8E9281B7A3700598866 /* DiscoveryCommunityViewModel.swift in Sources */, D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */, @@ -3882,7 +3875,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/Diffable/Discovery/DiscoveryItem.swift b/Mastodon/Diffable/Discovery/DiscoveryItem.swift index 024c4a2da..c817c20a0 100644 --- a/Mastodon/Diffable/Discovery/DiscoveryItem.swift +++ b/Mastodon/Diffable/Discovery/DiscoveryItem.swift @@ -7,11 +7,10 @@ import Foundation import MastodonSDK -import CoreDataStack enum DiscoveryItem: Hashable { case hashtag(Mastodon.Entity.Tag) case link(Mastodon.Entity.Link) - case user(ManagedObjectRecord) + case user(Mastodon.Entity.Account) case bottomLoader } diff --git a/Mastodon/Diffable/Discovery/DiscoverySection.swift b/Mastodon/Diffable/Discovery/DiscoverySection.swift index bb93ffc28..22b74ca80 100644 --- a/Mastodon/Diffable/Discovery/DiscoverySection.swift +++ b/Mastodon/Diffable/Discovery/DiscoverySection.swift @@ -57,25 +57,22 @@ extension DiscoverySection { return cell case .user(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ProfileCardTableViewCell.self), for: indexPath) as! ProfileCardTableViewCell - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - cell.configure( - tableView: tableView, - user: user, - profileCardTableViewCellDelegate: configuration.profileCardTableViewCellDelegate - ) - // bind familiarFollowers - if let familiarFollowers = configuration.familiarFollowers { - familiarFollowers - .map { array in array.first(where: { $0.id == user.id }) } - .assign(to: \.familiarFollowers, on: cell.profileCardView.viewModel) - .store(in: &cell.disposeBag) - } else { - cell.profileCardView.viewModel.familiarFollowers = nil - } - // bind me - cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) + cell.configure( + tableView: tableView, + user: record, + profileCardTableViewCellDelegate: configuration.profileCardTableViewCellDelegate + ) + // bind familiarFollowers + if let familiarFollowers = configuration.familiarFollowers { + familiarFollowers + .map { array in array.first(where: { $0.id == record.id }) } + .assign(to: \.familiarFollowers, on: cell.profileCardView.viewModel) + .store(in: &cell.disposeBag) + } else { + cell.profileCardView.viewModel.familiarFollowers = nil } + // bind me + cell.profileCardView.viewModel.relationshipViewModel.me = configuration.authContext.mastodonAuthenticationBox.inMemoryCache.meAccount return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell diff --git a/Mastodon/Diffable/Notification/NotificationItem.swift b/Mastodon/Diffable/Notification/NotificationItem.swift index b0fdddb7f..0c5f86c59 100644 --- a/Mastodon/Diffable/Notification/NotificationItem.swift +++ b/Mastodon/Diffable/Notification/NotificationItem.swift @@ -5,12 +5,11 @@ // Created by sxiaojian on 2021/4/13. // -import CoreData import Foundation -import CoreDataStack +import MastodonSDK enum NotificationItem: Hashable { - case feed(record: ManagedObjectRecord) - case feedLoader(record: ManagedObjectRecord) + case feed(record: FeedItem) + case feedLoader(record: FeedItem) case bottomLoader } diff --git a/Mastodon/Diffable/Notification/NotificationSection.swift b/Mastodon/Diffable/Notification/NotificationSection.swift index 0271aac20..569a307e8 100644 --- a/Mastodon/Diffable/Notification/NotificationSection.swift +++ b/Mastodon/Diffable/Notification/NotificationSection.swift @@ -43,16 +43,13 @@ extension NotificationSection { switch item { case .feed(let record): 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(record)), + 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..011dba90c 100644 --- a/Mastodon/Diffable/Report/ReportItem.swift +++ b/Mastodon/Diffable/Report/ReportItem.swift @@ -6,13 +6,13 @@ // import Foundation -import CoreDataStack +import MastodonSDK enum ReportItem: Hashable { case header(context: HeaderContext) - case status(record: ManagedObjectRecord) + case status(record: Mastodon.Entity.Status) case comment(context: CommentContext) - case result(record: ManagedObjectRecord) + case result(record: Mastodon.Entity.Account) case bottomLoader } diff --git a/Mastodon/Diffable/Status/StatusItem.swift b/Mastodon/Diffable/Status/StatusItem.swift index 1d08ea41d..1b4a8cf38 100644 --- a/Mastodon/Diffable/Status/StatusItem.swift +++ b/Mastodon/Diffable/Status/StatusItem.swift @@ -6,13 +6,13 @@ // 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: FeedItem) + case feedLoader(record: FeedItem) + case status(record: Mastodon.Entity.Status) case thread(Thread) case topLoader case bottomLoader @@ -24,7 +24,7 @@ extension StatusItem { case reply(context: Context) case leaf(context: Context) - public var record: ManagedObjectRecord { + public var record: Mastodon.Entity.Status { switch self { case .root(let threadContext), .reply(let threadContext), @@ -37,12 +37,12 @@ extension StatusItem { extension StatusItem.Thread { class Context: Hashable { - let status: ManagedObjectRecord + let status: Mastodon.Entity.Status var displayUpperConversationLink: Bool var displayBottomConversationLink: Bool init( - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, displayUpperConversationLink: Bool = false, displayBottomConversationLink: Bool = false ) { diff --git a/Mastodon/Diffable/Status/StatusSection.swift b/Mastodon/Diffable/Status/StatusSection.swift index 586764f42..383060389 100644 --- a/Mastodon/Diffable/Status/StatusSection.swift +++ b/Mastodon/Diffable/Status/StatusSection.swift @@ -6,8 +6,6 @@ // import Combine -import CoreData -import CoreDataStack import UIKit import AVKit import AlamofireImage @@ -46,40 +44,31 @@ extension StatusSection { switch item { case .feed(let record): 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(record)), + configuration: configuration + ) return cell case .feedLoader(let record): 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: record, + configuration: configuration + ) return cell case .status(let record): 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(record)), + configuration: configuration + ) return cell case .thread(let thread): let cell = dequeueConfiguredReusableCell( @@ -124,30 +113,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 } } @@ -161,12 +144,11 @@ extension StatusSection { authContext: AuthContext, statusView: StatusView ) { - let managedObjectContext = context.managedObjectContext statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource(tableView: statusView.pollTableView) { tableView, indexPath, item in switch item { case .history: - return nil - case .option(let record): + return UITableViewCell() + case .option(let record, let poll): // Fix cell reuse animation issue let cell: PollOptionTableViewCell = { let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self) + "@\(indexPath.row)#\(indexPath.section)") as? PollOptionTableViewCell @@ -176,53 +158,8 @@ extension StatusSection { cell.pollOptionView.viewModel.authContext = authContext - managedObjectContext.performAndWait { - guard let option = record.object(in: managedObjectContext) else { - assertionFailure() - return - } - - cell.pollOptionView.configure(pollOption: option) - - // trigger update if needs - let needsUpdatePoll: Bool = { - // check first option in poll to trigger update poll only once - guard - let poll = option.poll, - option.index == 0 - else { return false } + cell.pollOptionView.configure(status: statusView.viewModel.originalStatus!, pollOption: record, poll: poll) - guard !poll.expired else { - return false - } - - let now = Date() - let timeIntervalSinceUpdate = now.timeIntervalSince(poll.updatedAt) - #if DEBUG - let autoRefreshTimeInterval: TimeInterval = 3 // speedup testing - #else - let autoRefreshTimeInterval: TimeInterval = 30 - #endif - - guard timeIntervalSinceUpdate > autoRefreshTimeInterval else { - return false - } - - return true - }() - - if needsUpdatePoll { - guard let poll = option.poll else { return } - let pollRecord: ManagedObjectRecord = .init(objectID: poll.objectID) - Task { [weak context] in - guard let context = context else { return } - _ = try await context.apiService.poll( - poll: pollRecord, - authenticationBox: authContext.mastodonAuthenticationBox - ) - } - } - } // end managedObjectContext.performAndWait return cell } } @@ -319,7 +256,7 @@ extension StatusSection { static func configure( cell: TimelineMiddleLoaderTableViewCell, - feed: Feed, + feed: FeedItem, configuration: Configuration ) { cell.configure( diff --git a/Mastodon/Diffable/User/UserItem.swift b/Mastodon/Diffable/User/UserItem.swift index ba44aa52a..ea9b3f89e 100644 --- a/Mastodon/Diffable/User/UserItem.swift +++ b/Mastodon/Diffable/User/UserItem.swift @@ -11,7 +11,7 @@ import CoreDataStack import MastodonSDK enum UserItem: Hashable { - case user(record: ManagedObjectRecord) + case user(record: Mastodon.Entity.Account) 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..f58d93d26 100644 --- a/Mastodon/Diffable/User/UserSection.swift +++ b/Mastodon/Diffable/User/UserSection.swift @@ -6,13 +6,12 @@ // import UIKit -import CoreData -import CoreDataStack import MastodonCore import MastodonUI import MastodonMeta import MetaTextKit import Combine +import MastodonSDK enum UserSection: Hashable { case main @@ -37,7 +36,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.inMemoryCache.meAccount else { return cell } cell.userView.setButtonState(.loading) cell.configure( @@ -53,14 +52,13 @@ extension UserSection { 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, + user: record, followedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followingUserIds.eraseToAnyPublisher(), blockedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$blockedUserIds.eraseToAnyPublisher(), followRequestedUsers: authContext.mastodonAuthenticationBox.inMemoryCache.$followRequestedUserIDs.eraseToAnyPublisher() @@ -94,7 +92,7 @@ extension UserSection { userTableViewCellDelegate: UserTableViewCellDelegate? ) { cell.configure( - me: authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext), + me: authContext.mastodonAuthenticationBox.inMemoryCache.meAccount, tableView: tableView, viewModel: viewModel, delegate: userTableViewCellDelegate diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 2c54653ba..bcca2811f 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -6,14 +6,13 @@ // import UIKit -import CoreData -import CoreDataStack +import MastodonSDK import MastodonCore extension DataSourceFacade { public static func responseToStatusBookmarkAction( provider: UIViewController & NeedsDependency & AuthContextProvider, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) 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..8ebadbc28 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -6,14 +6,13 @@ // import UIKit -import CoreData -import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { public static func responseToStatusFavoriteAction( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index 6fe0005a0..6426ac42a 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -6,8 +6,6 @@ // import UIKit -import CoreDataStack -import class CoreDataStack.Notification import MastodonCore import MastodonSDK import MastodonLocalization @@ -15,7 +13,7 @@ import MastodonLocalization extension DataSourceFacade { static func responseToUserFollowAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + user: Mastodon.Entity.Account ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() @@ -26,86 +24,61 @@ extension DataSourceFacade { ) dependency.context.authenticationService.fetchFollowingAndBlockedAsync() } - - static func responseToUserFollowAction( - dependency: NeedsDependency & AuthContextProvider, - user: Mastodon.Entity.Account - ) 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() - } - } extension DataSourceFacade { static func responseToUserFollowRequestAction( dependency: NeedsDependency & AuthContextProvider, - notification: ManagedObjectRecord, + notification: Mastodon.Entity.Notification, 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 - } - guard let userID = _userID else { - assertionFailure() - 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 - } - - 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) - } - } +// let state: MastodonFollowRequestState = try await managedObjectContext.perform { +// guard let notification = notification.object(in: managedObjectContext) else { return .init(state: .none) } +// return 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) +// } +// } do { _ = try await dependency.context.apiService.followRequest( - userID: userID, + userID: notification.account.id, query: query, authenticationBox: dependency.authContext.mastodonAuthenticationBox ) } catch { // reset state when failure - try? await managedObjectContext.performChanges { - guard let notification = notification.object(in: managedObjectContext) else { return } - notification.transientFollowRequestState = .init(state: .none) - } +// try? await managedObjectContext.performChanges { +// guard let notification = notification.object(in: managedObjectContext) else { return } +// 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 +// 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) +// } default: let alertController = await UIAlertController(for: error, title: nil, preferredStyle: .alert) let okAction = await UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default) @@ -121,38 +94,38 @@ 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 - } - } +// 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 +// } +// } - 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) - } - } +// 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 { static func responseToShowHideReblogAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + user: Mastodon.Entity.Account ) async throws { _ = try await dependency.context.apiService.toggleShowReblogs( for: user, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift index 2422adf6c..69223f2ad 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Hashtag.swift @@ -19,8 +19,6 @@ extension DataSourceFacade { switch tag { case .entity(let entity): await coordinateToHashtagScene(provider: provider, tag: entity) - case .record(let record): - await coordinateToHashtagScene(provider: provider, tag: record) } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Media.swift index 8379f08e9..21e67d8fd 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,14 +62,13 @@ extension DataSourceFacade { @MainActor static func coordinateToMediaPreviewScene( dependency: NeedsDependency & MediaPreviewableViewController, - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, 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 + return status.mastodonAttachments } let thumbnails = await previewContext.thumbnails() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift index ca3bbd474..01ef9782b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Meta.swift @@ -6,20 +6,19 @@ // import Foundation -import CoreDataStack import MetaTextKit import MastodonCore +import MastodonSDK extension DataSourceFacade { static func responseToMetaTextAction( provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, meta: Meta ) async throws { - let _redirectRecord = await DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, + let _redirectRecord = DataSourceFacade.status( status: status, target: target ) @@ -35,7 +34,7 @@ extension DataSourceFacade { static func responseToMetaTextAction( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, meta: Meta ) async { switch meta { diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift index efdf41dbd..746727d8e 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Model.swift @@ -6,44 +6,14 @@ // import Foundation -import CoreData -import CoreDataStack import MastodonUI +import MastodonSDK extension DataSourceFacade { static func status( - managedObjectContext: NSManagedObjectContext, - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, 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? { + ) -> Mastodon.Entity.Status? { switch target { case .status: return status.reblog ?? status @@ -52,3 +22,13 @@ extension DataSourceFacade { } } } + +extension DataSourceFacade { + static func author( + status: Mastodon.Entity.Status, + target: StatusTarget + ) -> Mastodon.Entity.Account? { + DataSourceFacade.status(status: status, target: target) + .flatMap { $0.account } + } +} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 1db94bd4f..3a9071b6a 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -6,13 +6,13 @@ // import UIKit -import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { static func responseToUserMuteAction( dependency: NeedsDependency & AuthContextProvider, - user: ManagedObjectRecord + user: Mastodon.Entity.Account ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift index 30c024f54..b7028cc70 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Profile.swift @@ -6,19 +6,18 @@ // import UIKit -import CoreDataStack import MastodonCore import MastodonSDK +import CoreDataStack extension DataSourceFacade { static func coordinateToProfileScene( provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) async { - let _redirectRecord = await DataSourceFacade.author( - managedObjectContext: provider.context.managedObjectContext, + let _redirectRecord = DataSourceFacade.author( status: status, target: target ) @@ -35,17 +34,12 @@ extension DataSourceFacade { @MainActor static func coordinateToProfileScene( provider: ViewControllerWithDependencies & AuthContextProvider, - user: ManagedObjectRecord + user: Mastodon.Entity.Account ) async { - guard let user = user.object(in: provider.context.managedObjectContext) else { - assertionFailure() - return - } - - let profileViewModel = CachedProfileViewModel( + let profileViewModel = ProfileViewModel( context: provider.context, authContext: provider.authContext, - mastodonUser: user + optionalMastodonUser: user ) _ = provider.coordinator.present( @@ -71,9 +65,8 @@ extension DataSourceFacade { authenticationBox: provider.authContext.mastodonAuthenticationBox) provider.coordinator.hideLoading() - if let user { - await coordinateToProfileScene(provider: provider, user: user.asRecord) - } + await coordinateToProfileScene(provider: provider, user: user) + } catch { provider.coordinator.hideLoading() } @@ -85,12 +78,10 @@ extension DataSourceFacade { static func coordinateToProfileScene( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, mention: String, // username, userInfo: [AnyHashable: Any]? ) async { - let domain = provider.authContext.mastodonAuthenticationBox.domain - guard let href = userInfo?["href"] as? String, let url = URL(string: href) @@ -98,10 +89,7 @@ extension DataSourceFacade { return } - let managedObjectContext = provider.context.managedObjectContext - let mentions = try? await managedObjectContext.perform { - return status.object(in: managedObjectContext)?.mentions ?? [] - } + let mentions = status.mentions guard let mention = mentions?.first(where: { $0.url == href }) else { _ = await provider.coordinator.present( @@ -119,16 +107,7 @@ extension DataSourceFacade { 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 CachedProfileViewModel(context: provider.context, authContext: provider.authContext, mastodonUser: user) - } else { - return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID) - } + return RemoteProfileViewModel(context: provider.context, authContext: provider.authContext, userID: userID) }() _ = await provider.coordinator.present( @@ -154,11 +133,10 @@ extension DataSourceFacade { static func createActivityViewController( dependency: NeedsDependency, - user: ManagedObjectRecord + user: Mastodon.Entity.Account ) 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 { @@ -173,7 +151,7 @@ extension DataSourceFacade { return activityViewController } - static func createActivityViewControllerForMastodonUser(status: Status, dependency: NeedsDependency) -> UIActivityViewController { + static func createActivityViewControllerForMastodonUser(status: Mastodon.Entity.Status, dependency: NeedsDependency) -> UIActivityViewController { let activityViewController = UIActivityViewController( activityItems: status.activityItems, applicationActivities: [SafariActivity(sceneCoordinator: dependency.coordinator)] diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index ff3e95820..a02149bd5 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -6,14 +6,14 @@ // import UIKit -import CoreDataStack import MastodonCore import MastodonUI +import MastodonSDK extension DataSourceFacade { static func responseToStatusReblogAction( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) async throws { let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() await selectionFeedbackGenerator.selectionChanged() diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 31206c262..ff97ffd19 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -22,71 +22,60 @@ extension DataSourceFacade { break // not create search history for status case .user(let record): let authenticationBox = provider.authContext.mastodonAuthenticationBox - let managedObjectContext = provider.context.backgroundManagedObjectContext - - try? await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - guard let user = record.object(in: managedObjectContext) else { return } - _ = Persistence.SearchHistory.createOrMerge( - in: managedObjectContext, - context: Persistence.SearchHistory.PersistContext( - entity: .user(user), - me: me, - now: Date() - ) - ) - } // end try? await managedObjectContext.performChanges { … } + assertionFailure("Implement storing search history") case .hashtag(let tag): let authenticationBox = provider.authContext.mastodonAuthenticationBox let managedObjectContext = provider.context.backgroundManagedObjectContext switch tag { case .entity(let entity): - try? await managedObjectContext.performChanges { - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - - let now = Date() - - let result = Persistence.Tag.createOrMerge( - in: managedObjectContext, - context: Persistence.Tag.PersistContext( - domain: authenticationBox.domain, - entity: entity, - me: me, - networkDate: now - ) - ) - - _ = Persistence.SearchHistory.createOrMerge( - in: managedObjectContext, - context: Persistence.SearchHistory.PersistContext( - entity: .hashtag(result.tag), - me: me, - now: now - ) - ) - } // end try? await managedObjectContext.performChanges { … } - case .record(let record): - try? await managedObjectContext.performChanges { - let authenticationBox = provider.authContext.mastodonAuthenticationBox - guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } - guard let tag = record.object(in: managedObjectContext) else { return } - - let now = Date() + assertionFailure("Implement storing search history") - _ = Persistence.SearchHistory.createOrMerge( - in: managedObjectContext, - context: Persistence.SearchHistory.PersistContext( - entity: .hashtag(tag), - me: me, - now: now - ) - ) - } // end try? await managedObjectContext.performChanges { … } +// try? await managedObjectContext.performChanges { +// guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } +// +// let now = Date() +// +// let result = Persistence.Tag.createOrMerge( +// in: managedObjectContext, +// context: Persistence.Tag.PersistContext( +// domain: authenticationBox.domain, +// entity: entity, +// me: me, +// networkDate: now +// ) +// ) +// +// _ = Persistence.SearchHistory.createOrMerge( +// in: managedObjectContext, +// context: Persistence.SearchHistory.PersistContext( +// entity: .hashtag(result.tag), +// me: me, +// now: now +// ) +// ) +// } // end try? await managedObjectContext.performChanges { … } +// case .record(let record): +// try? await managedObjectContext.performChanges { +// let authenticationBox = provider.authContext.mastodonAuthenticationBox +// guard let me = authenticationBox.authentication.user(in: managedObjectContext) else { return } +// guard let tag = record.object(in: managedObjectContext) else { return } +// +// let now = Date() +// +// _ = Persistence.SearchHistory.createOrMerge( +// in: managedObjectContext, +// context: Persistence.SearchHistory.PersistContext( +// entity: .hashtag(tag), +// me: me, +// now: now +// ) +// ) +// } // end try? await managedObjectContext.performChanges { … } } // end switch tag { … } case .notification: assertionFailure() - } // end switch item { … } + } //end switch item { … } } // end func } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status+History.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status+History.swift index be48d726c..2517c20e3 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status+History.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status+History.swift @@ -7,7 +7,7 @@ import CoreDataStack extension DataSourceFacade { public static func getEditHistory( - forStatus status: Status, + forStatus status: Mastodon.Entity.Status, provider: NeedsDependency & AuthContextProvider ) async throws -> [Mastodon.Entity.StatusEdit] { let reponse = try await provider.context.apiService.getHistory(forStatusID: status.id, authenticationBox: provider.authContext.mastodonAuthenticationBox) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index d304816d0..6c60bb481 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -6,7 +6,6 @@ // import UIKit -import CoreDataStack import Alamofire import AlamofireImage import MastodonCore @@ -14,13 +13,14 @@ import MastodonUI import MastodonLocalization import LinkPresentation import UniformTypeIdentifiers +import MastodonSDK // Delete extension DataSourceFacade { static func responseToDeleteStatus( dependency: NeedsDependency & AuthContextProvider, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) async throws { _ = try await dependency.context.apiService.deleteStatus( status: status, @@ -36,7 +36,7 @@ extension DataSourceFacade { @MainActor public static func responseToStatusShareAction( provider: DataSourceProvider, - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, button: UIButton ) async throws { let activityViewController = try await createActivityViewController( @@ -56,22 +56,21 @@ extension DataSourceFacade { private static func createActivityViewController( dependency: NeedsDependency, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) 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.url ?? status.uri) else { return [] } return [ URLActivityItemWithMetadata(url: url) { metadata in - metadata.title = "\(status.author.displayName) (@\(status.author.acctWithDomain))" + metadata.title = "\(status.account.displayName) (@\(status.account.acctWithDomain))" metadata.iconProvider = ImageProvider( - url: status.author.avatarImageURLWithFallback(domain: status.author.domain), + url: status.account.avatarImageURLWithFallback(domain: status.account.domain!), filter: ScaledToSizeFilter(size: CGSize.authorAvatarButtonSize) ).itemProvider } ] as [Any] - } + }() var applicationActivities: [UIActivity] = [ SafariActivity(sceneCoordinator: dependency.coordinator), // open URL ] @@ -94,20 +93,20 @@ extension DataSourceFacade { @MainActor static func responseToActionToolbar( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, 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 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 +// } switch action { case .reply: @@ -150,7 +149,7 @@ extension DataSourceFacade { extension DataSourceFacade { struct MenuContext { - let author: ManagedObjectRecord? + let author: Mastodon.Entity.Account? let statusViewModel: StatusView.ViewModel? let button: UIButton? let barButtonItem: UIBarButtonItem? @@ -178,17 +177,9 @@ extension DataSourceFacade { title: actionTitle, style: .destructive ) { [weak dependency] _ in - guard let dependency else { return } + guard let dependency, let user = menuContext.author 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 @@ -214,12 +205,7 @@ extension DataSourceFacade { ) { [weak dependency] _ in guard let dependency = 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 } + guard let user = menuContext.author else { return } try await DataSourceFacade.responseToUserMuteAction( dependency: dependency, user: user @@ -242,12 +228,7 @@ extension DataSourceFacade { ) { [weak dependency] _ in guard let dependency = 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 } + guard let user = menuContext.author else { return } try await DataSourceFacade.responseToUserBlockAction( dependency: dependency, user: user @@ -266,7 +247,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 +278,7 @@ extension DataSourceFacade { ) case .bookmarkStatus: Task { - guard let status = menuContext.statusViewModel?.originalStatus?.asRecord else { + guard let status = menuContext.statusViewModel?.originalStatus else { assertionFailure() return } @@ -308,13 +289,7 @@ extension DataSourceFacade { } // end Task 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 { - assertionFailure() + guard let status = menuContext.statusViewModel?.originalStatus else { return } @@ -344,7 +319,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 +333,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 +346,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,13 +377,13 @@ extension DataSourceFacade { static func responseToToggleSensitiveAction( dependency: NeedsDependency, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) 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) - } +// try await dependency.context.managedObjectContext.perform { +// let _status = status.reblog ?? status +// status.update(isSensitiveToggled: !_status.sensitiveToggled) +// } + assertionFailure("Net yet implemented :-(") } } diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift index ad8d0e671..6807c2411 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Thread.swift @@ -6,19 +6,17 @@ // import UIKit -import CoreData -import CoreDataStack import MastodonCore +import MastodonSDK extension DataSourceFacade { static func coordinateToStatusThreadScene( provider: ViewControllerWithDependencies & AuthContextProvider, target: StatusTarget, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) async { - let _root: StatusItem.Thread? = await { - let _redirectRecord = await DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, + let _root: StatusItem.Thread? = { + let _redirectRecord = DataSourceFacade.status( status: status, target: target ) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift index 912540f69..6ea08264b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift @@ -6,8 +6,6 @@ // import UIKit -import CoreData -import CoreDataStack import MastodonCore import MastodonSDK @@ -20,27 +18,21 @@ extension DataSourceFacade { public static func translateStatus( provider: Provider, - status: ManagedObjectRecord + status: Mastodon.Entity.Status ) 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) + return try await _translateStatus(provider: provider, status: reblog) } else { - return try await translateStatus(provider: provider, status: status) + return try await _translateStatus(provider: provider, status: status) } } } private extension DataSourceFacade { - static func translateStatus(provider: Provider, status: Status) async throws -> Mastodon.Entity.Translation? { + static func _translateStatus(provider: Provider, status: Mastodon.Entity.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..fe3c808ef 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: Mastodon.Entity.Status, url: URL ) async { let domain = provider.authContext.mastodonAuthenticationBox.domain diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift index a90bdd0a8..5e3415986 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+UserView.swift @@ -2,69 +2,10 @@ import Foundation import MastodonUI -import CoreDataStack import MastodonCore 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, diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index 0974510bf..4d2d5c63e 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 { @@ -30,20 +31,11 @@ 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 { - assertionFailure() - return - } - try await DataSourceFacade.responseToMenuAction( dependency: self, action: action, menuContext: .init( - author: author, + author: notification.account, statusViewModel: nil, button: button, barButtonItem: nil @@ -70,17 +62,9 @@ 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 } - return .init(objectID: notification.account.objectID) - } - guard let author = _author else { - assertionFailure() - return - } await DataSourceFacade.coordinateToProfileScene( provider: self, - user: author + user: notification.account ) } // end Task } @@ -155,7 +139,7 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut } private struct NotificationMediaTransitionContext { - let status: ManagedObjectRecord + let status: Mastodon.Entity.Status let needsToggleMediaSensitive: Bool } @@ -175,23 +159,17 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med assertionFailure() return } - guard case let .notification(record) = item else { + guard case let .notification(record) = item, let _status = record.status else { assertionFailure("only works for status data provider") 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 - return NotificationMediaTransitionContext( - status: .init(objectID: status.objectID), - needsToggleMediaSensitive: status.isSensitiveToggled ? !status.sensitive : status.sensitive - ) - } - - guard let mediaTransitionContext = _mediaTransitionContext else { return } + let status = _status.reblog ?? _status + let mediaTransitionContext = NotificationMediaTransitionContext( + status: status, + needsToggleMediaSensitive: status.sensitiveToggled ? !(status.sensitive == true) : status.sensitive == true + ) guard !mediaTransitionContext.needsToggleMediaSensitive else { try await DataSourceFacade.responseToToggleSensitiveAction( @@ -227,23 +205,18 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Med assertionFailure() return } - guard case let .notification(record) = item else { + guard case let .notification(record) = item, let _status = record.status else { assertionFailure("only works for status data provider") 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 - return NotificationMediaTransitionContext( - status: .init(objectID: status.objectID), - needsToggleMediaSensitive: status.isMediaSensitive ? !status.isSensitiveToggled : false - ) - } - - guard let mediaTransitionContext = _mediaTransitionContext else { return } + + let status = _status.reblog ?? _status + let mediaTransitionContext = NotificationMediaTransitionContext( + status: status, + needsToggleMediaSensitive: status.sensitiveToggled ? !status.sensitiveToggled : false + ) guard !mediaTransitionContext.needsToggleMediaSensitive else { try await DataSourceFacade.responseToToggleSensitiveAction( @@ -286,11 +259,8 @@ 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) - } + let _status = notification.status + guard let status = _status else { assertionFailure() return @@ -323,12 +293,8 @@ 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 { + + guard let author = notification.status?.account else { assertionFailure() return } @@ -367,12 +333,8 @@ 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 else { assertionFailure() return } @@ -400,12 +362,8 @@ 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 else { assertionFailure() return } @@ -465,12 +423,8 @@ 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 else { assertionFailure() return } @@ -497,12 +451,8 @@ 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 else { assertionFailure() return } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 68be145e2..88d0d4d5d 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -6,13 +6,13 @@ // import UIKit -import CoreDataStack import MetaTextKit import MastodonCore import MastodonUI import MastodonLocalization import MastodonAsset import LinkPresentation +import MastodonSDK // MARK: - header extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { @@ -37,22 +37,15 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte case .none: 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 } - let request = MastodonUser.sortedFetchRequest - request.predicate = MastodonUser.predicate(domain: status.author.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 { - return - } + let account = try await context.apiService.accountLookup( + domain: authContext.mastodonAuthenticationBox.domain, + query: .init(acct: status.account.id), + authorization: authContext.mastodonAuthenticationBox.userAuthorization + ).singleOutput().value await DataSourceFacade.coordinateToProfileScene( provider: self, - user: replyToAuthor + user: account ) case .repost: @@ -184,7 +177,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 +199,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 } } ], @@ -313,54 +306,51 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte return } - var _poll: ManagedObjectRecord? - var _isMultiple: Bool? - var _choice: Int? - - try await managedObjectContext.performChanges { - guard let pollOption = pollOption.object(in: managedObjectContext) else { return } - guard let poll = pollOption.poll else { return } - _poll = .init(objectID: poll.objectID) - - _isMultiple = poll.multiple - guard !poll.isVoting else { return } - - if !poll.multiple { - for option in poll.options where option != pollOption { - option.update(isSelected: false) - } - - // mark voting - poll.update(isVoting: true) - // set choice - _choice = Int(pollOption.index) - } - - pollOption.update(isSelected: !pollOption.isSelected) - poll.update(updatedAt: Date()) - } +// var _poll: ManagedObjectRecord? +// var _isMultiple: Bool? +// var _choice: Int? +// +// try await managedObjectContext.performChanges { +// guard let pollOption = pollOption.object(in: managedObjectContext) else { return } +// guard let poll = pollOption.poll else { return } +// _poll = .init(objectID: poll.objectID) +// +// _isMultiple = poll.multiple +// guard !poll.isVoting else { return } +// +// if !poll.multiple { +// for option in poll.options where option != pollOption { +// option.update(isSelected: false) +// } +// +// // mark voting +// poll.update(isVoting: true) +// // set choice +// _choice = Int(pollOption.index) +// } +// +// pollOption.update(isSelected: !pollOption.isSelected) +// poll.update(updatedAt: Date()) +// } // Trigger vote API request for - guard let poll = _poll, - _isMultiple == false, - let choice = _choice - else { return } - + guard pollOption.poll.multiple == false else { return } + do { _ = try await context.apiService.vote( - poll: poll, - choices: [choice], + poll: pollOption.poll, + choices: [indexPath.row], authenticationBox: authContext.mastodonAuthenticationBox ) } catch { // restore voting state - try await managedObjectContext.performChanges { - guard - let pollOption = pollOption.object(in: managedObjectContext), - let poll = pollOption.poll - else { return } - poll.update(isVoting: false) - } +// try await managedObjectContext.performChanges { +// guard +// let pollOption = pollOption.object(in: managedObjectContext), +// let poll = pollOption.poll +// else { return } +// poll.update(isVoting: false) +// } } } // end Task @@ -373,48 +363,27 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte ) { guard let pollTableViewDiffableDataSource = statusView.pollTableViewDiffableDataSource else { return } guard let firstPollItem = pollTableViewDiffableDataSource.snapshot().itemIdentifiers.first else { return } - guard case let .option(firstPollOption) = firstPollItem else { return } + guard case let .option(option, poll) = firstPollItem else { return } let managedObjectContext = context.managedObjectContext Task { - var _poll: ManagedObjectRecord? - var _choices: [Int]? - - try await managedObjectContext.performChanges { - guard let poll = firstPollOption.object(in: managedObjectContext)?.poll else { return } - _poll = .init(objectID: poll.objectID) - - guard poll.multiple else { return } - - // mark voting - poll.update(isVoting: true) - // set choice - _choices = poll.options - .filter { $0.isSelected } - .map { Int($0.index) } - - poll.update(updatedAt: Date()) - } - // Trigger vote API request for - guard let poll = _poll, - let choices = _choices - else { return } - do { - _ = try await context.apiService.vote( - poll: poll, - choices: choices, - authenticationBox: authContext.mastodonAuthenticationBox - ) - } catch { - // restore voting state - try await managedObjectContext.performChanges { - guard let poll = poll.object(in: managedObjectContext) else { return } - poll.update(isVoting: false) - } - } + assertionFailure("Re-implement this") +// do { +// _ = try await context.apiService.vote( +// poll: poll, +// choices: choices, +// authenticationBox: authContext.mastodonAuthenticationBox +// ) +// } catch { +// // restore voting state +// try await managedObjectContext.performChanges { +// guard let poll = poll.object(in: managedObjectContext) else { return } +// poll.update(isVoting: false) +// } +// } } // end Task } @@ -470,16 +439,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte assertionFailure("only works for status data provider") 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 - 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 { @@ -513,7 +473,7 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte dependency: self, action: action, menuContext: .init( - author: author, + author: status.account, statusViewModel: statusViewModel, button: button, barButtonItem: nil @@ -679,11 +639,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..2d50f15ba 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -6,8 +6,8 @@ // import UIKit -import CoreDataStack import MastodonCore +import MastodonSDK extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & StatusTableViewControllerNavigateableRelay { @@ -55,7 +55,7 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvider & AuthContextProvider { @MainActor - private func statusRecord() async -> ManagedObjectRecord? { + private func statusRecord() async -> Mastodon.Entity.Status? { 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,15 +64,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 { - 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 0944cee6c..346deeeea 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -40,30 +40,17 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid tag: tag ) 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) - } - if let status = _status { + if let status = notification.status { await DataSourceFacade.coordinateToStatusThreadScene( provider: self, target: .status, // remove reblog wrapper 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) - } - if let author = _author { - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: author - ) - } + await DataSourceFacade.coordinateToProfileScene( + provider: self, + user: notification.account + ) } } } // end Task diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index b92aadcef..730fb373e 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -7,22 +7,19 @@ // import UIKit -import CoreDataStack import MastodonSDK -import class CoreDataStack.Notification enum DataSourceItem: Hashable { - case status(record: ManagedObjectRecord) - case user(record: ManagedObjectRecord) + case status(record: Mastodon.Entity.Status) + case user(record: Mastodon.Entity.Account) case hashtag(tag: TagKind) - case notification(record: ManagedObjectRecord) + case notification(record: Mastodon.Entity.Notification) case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?) } extension DataSourceItem { enum TagKind: Hashable { case entity(Mastodon.Entity.Tag) - case record(ManagedObjectRecord) } } diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 442a47de4..384685ea6 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: Mastodon.Entity.Status, statusSource: Mastodon.Entity.StatusSource) } var disposeBag = Set() diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift index a25dbaf1c..8081af3fc 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+Diffable.swift @@ -29,7 +29,7 @@ extension DiscoveryCommunityViewModel { stateMachine.enter(State.Reloading.self) - statusFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift index f61df078d..bb57d60db 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 newRecords = isReloading ? [] : viewModel.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !newRecords.contains(where: { $0.id == status.id }) else { continue } + newRecords.append(status) hasNewStatusesAppend = true } @@ -158,7 +158,7 @@ extension DiscoveryCommunityViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + viewModel.records = newRecords viewModel.didLoadLatest.send() } catch { diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift index 6169e0830..263942059 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel.swift @@ -21,7 +21,7 @@ final class DiscoveryCommunityViewModel { let context: AppContext let authContext: AuthContext let viewDidAppeared = PassthroughSubject() - let statusFetchedResultsController: StatusFetchedResultsController + @Published var records = [Mastodon.Entity.Status]() let listBatchFetchViewModel = ListBatchFetchViewModel() // output @@ -44,11 +44,5 @@ final class DiscoveryCommunityViewModel { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) - // end init } } diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift index 952ba4937..38ec6f589 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift @@ -97,11 +97,10 @@ extension DiscoveryForYouViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } - guard let user = record.object(in: context.managedObjectContext) else { return } - let profileViewModel = CachedProfileViewModel( + let profileViewModel = ProfileViewModel( context: context, authContext: viewModel.authContext, - mastodonUser: user + optionalMastodonUser: record ) _ = coordinator.present( scene: .profile(viewModel: profileViewModel), @@ -137,9 +136,8 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate { ) { guard let indexPath = tableView.indexPath(for: cell) else { return } guard case let .user(record) = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { return } - guard let user = record.object(in: context.managedObjectContext) else { return } - let userID = user.id + let userID = record.id let _familiarFollowers = viewModel.familiarFollowers.first(where: { $0.id == userID }) guard let familiarFollowers = _familiarFollowers else { assertionFailure() diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift index 31b14c55d..2116ea528 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel+Diffable.swift @@ -29,7 +29,7 @@ extension DiscoveryForYouViewModel { try await fetch() } - userFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift index fcd521a36..784e1d851 100644 --- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift +++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewModel.swift @@ -20,12 +20,13 @@ final class DiscoveryForYouViewModel { // input let context: AppContext let authContext: AuthContext - let userFetchedResultsController: UserFetchedResultsController +// let userFetchedResultsController: UserFetchedResultsController @MainActor @Published var familiarFollowers: [Mastodon.Entity.FamiliarFollowers] = [] @Published var isFetching = false - + @Published var records = [Mastodon.Entity.Account]() + // output var diffableDataSource: UITableViewDiffableDataSource? let didLoadLatest = PassthroughSubject() @@ -33,11 +34,11 @@ final class DiscoveryForYouViewModel { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalPredicate: nil - ) +// self.userFetchedResultsController = UserFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalPredicate: nil +// ) // end init } } @@ -58,7 +59,7 @@ extension DiscoveryForYouViewModel { authenticationBox: authContext.mastodonAuthenticationBox ) familiarFollowers = _familiarFollowersResponse?.value ?? [] - userFetchedResultsController.userIDs = userIDs +// userFetchedResultsController.userIDs = userIDs } catch { // do nothing } diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift index 99d68796d..ecbfb9495 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+Diffable.swift @@ -29,7 +29,7 @@ extension DiscoveryPostsViewModel { stateMachine.enter(State.Reloading.self) - statusFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift index 75794258d..3adef797c 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 newStatuses = isReloading ? [] : viewModel.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !newStatuses.contains(where: { $0.id == status.id }) else { continue } + newStatuses.append(status) hasNewStatusesAppend = true } @@ -155,7 +155,7 @@ extension DiscoveryPostsViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + viewModel.records = newStatuses viewModel.didLoadLatest.send() } catch { diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift index 3024f03be..b0d0da39c 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel.swift @@ -20,7 +20,7 @@ final class DiscoveryPostsViewModel { // input let context: AppContext let authContext: AuthContext - let statusFetchedResultsController: StatusFetchedResultsController + @Published var records = [Mastodon.Entity.Status]() let listBatchFetchViewModel = ListBatchFetchViewModel() // output @@ -44,12 +44,6 @@ final class DiscoveryPostsViewModel { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) - // end init Task { await checkServerEndpoint() diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift index 8cc185382..e7c03d363 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift @@ -34,7 +34,7 @@ extension HashtagTimelineViewModel { snapshot.appendSections([.main]) diffableDataSource?.apply(snapshot) - fetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index 579060bda..adb9cb5c5 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -7,7 +7,7 @@ import Foundation import GameplayKit -import CoreDataStack +import MastodonSDK extension HashtagTimelineViewModel { class State: GKState { @@ -93,7 +93,7 @@ extension HashtagTimelineViewModel.State { } class Loading: HashtagTimelineViewModel.State { - var maxID: Status.ID? + var maxID: Mastodon.Entity.Status.ID? override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { @@ -145,10 +145,10 @@ extension HashtagTimelineViewModel.State { self.maxID = newMaxID var hasNewStatusesAppend = false - var statusIDs = isReloading ? [] : viewModel.fetchedResultsController.statusIDs + var newRecords = isReloading ? [] : viewModel.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !newRecords.contains(where: { $0.id == status.id }) else { continue } + newRecords.append(status) hasNewStatusesAppend = true } @@ -158,7 +158,7 @@ extension HashtagTimelineViewModel.State { await enter(state: NoMore.self) } - viewModel.fetchedResultsController.append(statusIDs: statusIDs) + viewModel.records = newRecords 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..11f0b6c77 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonSDK import MastodonCore @@ -24,7 +22,7 @@ final class HashtagTimelineViewModel { // input let context: AppContext let authContext: AuthContext - let fetchedResultsController: StatusFetchedResultsController +// let fetchedResultsController: StatusFetchedResultsController let isFetchingLatestTimeline = CurrentValueSubject(false) let timelinePredicate = CurrentValueSubject(nil) let hashtagEntity = CurrentValueSubject(nil) @@ -34,6 +32,8 @@ final class HashtagTimelineViewModel { var diffableDataSource: UITableViewDiffableDataSource? let didLoadLatest = PassthroughSubject() let hashtagDetails = CurrentValueSubject(nil) + + @Published var records = [Mastodon.Entity.Status]() // bottom loader private(set) lazy var stateMachine: GKStateMachine = { @@ -54,28 +54,30 @@ final class HashtagTimelineViewModel { self.context = context self.authContext = authContext self.hashtag = hashtag - self.fetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) +// self.fetchedResultsController = StatusFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalTweetPredicate: nil +// ) updateTagInformation() // end init } func viewWillAppear() { - let predicate = Tag.predicate( - domain: authContext.mastodonAuthenticationBox.domain, - name: hashtag - ) +// let predicate = Tag.predicate( +// domain: authContext.mastodonAuthenticationBox.domain, +// name: hashtag +// ) + + #warning("Re-Implement this") guard - let object = Tag.findOrFetch(in: context.managedObjectContext, matching: predicate) + false//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: object.following)) } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift index b141d386a..5b2b83a16 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DataSourceProvider.swift @@ -20,17 +20,11 @@ extension HomeTimelineViewController: DataSourceProvider { } switch item { - case .feed(let record): - let managedObjectContext = context.managedObjectContext - let item: DataSourceItem? = try? await managedObjectContext.perform { - guard let feed = record.object(in: managedObjectContext) else { return nil } - guard feed.kind == .home else { return nil } - if let status = feed.status { - return .status(record: .init(objectID: status.objectID)) - } else { - return nil - } - } + case .feed(let record): + let item: DataSourceItem? = { + guard let status = record.status else { return nil } + return .status(record: status) + }() return item default: return nil diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 18cbf18d2..55b62e666 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -9,6 +9,7 @@ import UIKit import CoreData import CoreDataStack import MastodonUI +import MastodonSDK extension HomeTimelineViewModel { @@ -35,7 +36,7 @@ extension HomeTimelineViewModel { snapshot.appendSections([.main]) diffableDataSource?.apply(snapshot) - fetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } @@ -45,7 +46,7 @@ extension HomeTimelineViewModel { let oldSnapshot = diffableDataSource.snapshot() var newSnapshot: NSDiffableDataSourceSnapshot = { let newItems = records.map { record in - StatusItem.feed(record: record) + StatusItem.feed(record: .init(status: record, notification: nil, hasMore: false, isLoadingMore: false)) } var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) @@ -53,36 +54,37 @@ extension HomeTimelineViewModel { 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("Code below needs to be re-implemented") +// 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..16a64ddef 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -7,10 +7,9 @@ import func QuartzCore.CACurrentMediaTime import Foundation -import CoreData -import CoreDataStack import GameplayKit import MastodonCore +import MastodonSDK extension HomeTimelineViewModel { class LoadLatestState: GKState { @@ -83,15 +82,11 @@ 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 + let latestFeedRecords = viewModel.records.prefix(APIService.onceRequestStatusMaxCount) Task { - let latestStatusIDs: [Status.ID] = latestFeedRecords.compactMap { record in - guard let feed = record.object(in: managedObjectContext) else { return nil } - return feed.status?.id + let latestStatusIDs: [Mastodon.Entity.Status.ID] = latestFeedRecords.compactMap { record in + return record.id } do { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 1b6e4499d..48d92a955 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -31,7 +31,7 @@ extension HomeTimelineViewModel.LoadOldestState { class Initial: HomeTimelineViewModel.LoadOldestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { guard let viewModel = viewModel else { return false } - guard !viewModel.fetchedResultsController.records.isEmpty else { return false } + guard !viewModel.records.isEmpty else { return false } return stateClass == Loading.self } } @@ -46,19 +46,13 @@ extension HomeTimelineViewModel.LoadOldestState { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let lastFeedRecord = viewModel.fetchedResultsController.records.last else { + guard let lastFeedRecord = viewModel.records.last else { stateMachine.enter(Idle.self) return } 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 } - return status.id - } + let _maxID: Mastodon.Entity.Status.ID? = viewModel.records.last?.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..1c6645156 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -9,12 +9,11 @@ import func AVFoundation.AVMakeRect import UIKit import AVKit import Combine -import CoreData -import CoreDataStack import GameplayKit import AlamofireImage import MastodonCore import MastodonUI +import MastodonSDK final class HomeTimelineViewModel: NSObject { @@ -24,7 +23,7 @@ final class HomeTimelineViewModel: NSObject { // input let context: AppContext let authContext: AuthContext - let fetchedResultsController: FeedFetchedResultsController +// let fetchedResultsController: FeedFetchedResultsController let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -34,6 +33,7 @@ final class HomeTimelineViewModel: NSObject { @Published var scrollPositionRecord: ScrollPositionRecord? = nil @Published var displaySettingBarButtonItem = true @Published var hasPendingStatusEditReload = false + @Published var records = [Mastodon.Entity.Status]() weak var tableView: UITableView? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? @@ -80,14 +80,19 @@ final class HomeTimelineViewModel: NSObject { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) +// self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context) super.init() - fetchedResultsController.predicate = Feed.predicate( - kind: .home, - acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID) - ) +// fetchedResultsController.predicate = Feed.predicate( +// kind: .home, +// acct: .mastodon(domain: authContext.mastodonAuthenticationBox.domain, userID: authContext.mastodonAuthenticationBox.userID) +// ) + + Task { + records = try await context.apiService.homeTimeline(authenticationBox: authContext.mastodonAuthenticationBox) + .value + } homeTimelineNeedRefresh .sink { [weak self] _ in @@ -116,7 +121,15 @@ extension HomeTimelineViewModel { extension HomeTimelineViewModel { func timelineDidReachEnd() { - fetchedResultsController.fetchNextBatch() +// fetchedResultsController.fetchNextBatch() + Task { + let newRecords = try await context.apiService.homeTimeline( + sinceID: records.last?.id, + authenticationBox: authContext.mastodonAuthenticationBox + ).value + + records += newRecords + } } } @@ -124,29 +137,29 @@ extension HomeTimelineViewModel { // load timeline gap func loadMore(item: StatusItem) async { - guard case let .feedLoader(record) = item else { return } + guard case let .feedLoader(record) = item, let status = record.status else { return } 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@\(status.id)" - // 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 feed = record.object(in: managedObjectContext) else { return } +// guard let status = feed.status 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) +// } // reconfigure item snapshot.reconfigureItems([item]) @@ -160,14 +173,14 @@ extension HomeTimelineViewModel { authenticationBox: authContext.mastodonAuthenticationBox ) } catch { - do { - // restore state - try await managedObjectContext.performChanges { - feed.update(isLoadingMore: false) - } - } catch { - assertionFailure(error.localizedDescription) - } +// do { +// // restore state +// try await managedObjectContext.performChanges { +// feed.update(isLoadingMore: false) +// } +// } catch { +// assertionFailure(error.localizedDescription) +// } } // reconfigure item again diff --git a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift index 1d2b40ebc..72d70db63 100644 --- a/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Notification/Cell/NotificationTableViewCell+ViewModel.swift @@ -7,7 +7,7 @@ import UIKit import Combine -import CoreDataStack +import MastodonSDK extension NotificationTableViewCell { final class ViewModel { @@ -18,7 +18,7 @@ extension NotificationTableViewCell { } enum Value { - case feed(Feed) + case feed(FeedItem) } } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index c058ee921..169aa6c4d 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -21,16 +21,13 @@ extension NotificationTimelineViewController: DataSourceProvider { switch item { case .feed(let record): - let managedObjectContext = context.managedObjectContext - let item: DataSourceItem? = try? await managedObjectContext.perform { - guard let feed = record.object(in: managedObjectContext) else { return nil } - guard feed.kind == .notificationAll || feed.kind == .notificationMentions else { return nil } - if let notification = feed.notification { - return .notification(record: .init(objectID: notification.objectID)) + let item: DataSourceItem? = { + if let notification = record.notification { + return .notification(record: notification) } 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..1f9795253 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: status)) ) _ = self.coordinator.present( scene: .thread(viewModel: threadViewModel), diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index c412c39a4..c447b6976 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -30,7 +30,7 @@ extension NotificationTimelineViewModel { snapshot.appendSections([.main]) diffableDataSource?.apply(snapshot) - feedFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } @@ -40,7 +40,7 @@ extension NotificationTimelineViewModel { let oldSnapshot = diffableDataSource.snapshot() var newSnapshot: NSDiffableDataSourceSnapshot = { let newItems = records.map { record in - NotificationItem.feed(record: record) + NotificationItem.feed(record: .init(status: nil, notification: record, hasMore: false, isLoadingMore: false)) } var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) @@ -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("Code below needs to be re-implemented") +// 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..701d848be 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -5,7 +5,6 @@ // Created by MainasuK on 2022-1-21. // -import CoreDataStack import Foundation import GameplayKit import MastodonSDK @@ -32,7 +31,7 @@ extension NotificationTimelineViewModel.LoadOldestState { class Initial: NotificationTimelineViewModel.LoadOldestState { override func isValidNextState(_ stateClass: AnyClass) -> Bool { guard let viewModel = viewModel else { return false } - guard !viewModel.feedFetchedResultsController.records.isEmpty else { return false } + guard !viewModel.records.isEmpty else { return false } return stateClass == Loading.self } } @@ -47,7 +46,7 @@ extension NotificationTimelineViewModel.LoadOldestState { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let lastFeedRecord = viewModel.feedFetchedResultsController.records.last else { + guard let lastFeedRecord = viewModel.records.last else { stateMachine.enter(Fail.self) return } @@ -55,12 +54,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? = viewModel.records.last?.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..0345b609e 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import GameplayKit import MastodonSDK import MastodonCore @@ -20,10 +19,11 @@ final class NotificationTimelineViewModel { let context: AppContext let authContext: AuthContext let scope: Scope - let feedFetchedResultsController: FeedFetchedResultsController +// let feedFetchedResultsController: FeedFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @Published var isLoadingLatest = false @Published var lastAutomaticFetchTimestamp: Date? + @Published var records = [Mastodon.Entity.Notification]() // output var diffableDataSource: UITableViewDiffableDataSource? @@ -51,10 +51,15 @@ final class NotificationTimelineViewModel { self.context = context self.authContext = authContext self.scope = scope - self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) +// self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) // end init - feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate( +// feedFetchedResultsController.predicate = NotificationTimelineViewModel.feedPredicate( +// authenticationBox: authContext.mastodonAuthenticationBox, +// scope: scope +// ) + + loadNotifications( authenticationBox: authContext.mastodonAuthenticationBox, scope: scope ) @@ -67,40 +72,55 @@ extension NotificationTimelineViewModel { typealias Scope = APIService.MastodonNotificationScope - static func feedPredicate( + func loadNotifications( 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 + ) { + Task { + let notifications = try await context.apiService.notifications( + maxID: nil, + scope: scope, + authenticationBox: authenticationBox + ).value + + records = notifications + } } + +// 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 +// } } @@ -124,26 +144,26 @@ extension NotificationTimelineViewModel { // load timeline gap func loadMore(item: NotificationItem) async { - guard case let .feedLoader(record) = item else { return } + guard case let .feedLoader(record) = item, let notification = record.notification else { return } - let managedObjectContext = context.managedObjectContext - let key = "LoadMore@\(record.objectID)" +// let managedObjectContext = context.managedObjectContext + let key = "LoadMore@\(notification.id)" - // 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) - } +// // 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) +// } // fetch data do { _ = try await context.apiService.notifications( - maxID: maxID, + maxID: notification.id, scope: scope, authenticationBox: authContext.mastodonAuthenticationBox ) diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift index d52309e92..2e9626d1b 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+Diffable.swift @@ -32,7 +32,7 @@ extension BookmarkViewModel { stateMachine.enter(State.Reloading.self) - statusFetchedResultsController.$records + $statuses .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift index f746a5acb..5982021d7 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -57,7 +57,7 @@ extension BookmarkViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.statusFetchedResultsController.statusIDs = [] + viewModel.statuses = [] stateMachine.enter(Loading.self) } @@ -128,10 +128,10 @@ extension BookmarkViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var modifiedStatuses = viewModel.statuses for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !modifiedStatuses.map({ $0.id }).contains(status.id) else { continue } + modifiedStatuses.append(status) hasNewStatusesAppend = true } @@ -147,7 +147,7 @@ extension BookmarkViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + viewModel.statuses = modifiedStatuses } catch { await enter(state: Fail.self) } diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift index f56e65526..6bf60d770 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel.swift @@ -11,6 +11,7 @@ import CoreData import CoreDataStack import GameplayKit import MastodonCore +import MastodonSDK final class BookmarkViewModel { @@ -20,7 +21,8 @@ final class BookmarkViewModel { let context: AppContext let authContext: AuthContext - let statusFetchedResultsController: StatusFetchedResultsController +// let statusFetchedResultsController: StatusFetchedResultsController + @Published var statuses = [Mastodon.Entity.Status]() let listBatchFetchViewModel = ListBatchFetchViewModel() // output @@ -41,11 +43,11 @@ final class BookmarkViewModel { 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( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalTweetPredicate: nil +// ) } } diff --git a/Mastodon/Scene/Profile/CachedProfileViewModel.swift b/Mastodon/Scene/Profile/CachedProfileViewModel.swift deleted file mode 100644 index a769f2a9f..000000000 --- a/Mastodon/Scene/Profile/CachedProfileViewModel.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CachedProfileViewModel.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-31. -// - -import Foundation -import CoreDataStack -import MastodonCore - -final class CachedProfileViewModel: ProfileViewModel { - - init(context: AppContext, authContext: AuthContext, mastodonUser: MastodonUser) { - super.init(context: context, authContext: authContext, optionalMastodonUser: mastodonUser) - } -} diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift index 291a6b3ff..42c838c98 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel+Diffable.swift @@ -20,7 +20,7 @@ extension FamiliarFollowersViewModel { userTableViewCellDelegate: userTableViewCellDelegate ) - userFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift index 3e4a17e5e..e74d2b28c 100644 --- a/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift +++ b/Mastodon/Scene/Profile/FamiliarFollowers/FamiliarFollowersViewModel.swift @@ -17,8 +17,8 @@ final class FamiliarFollowersViewModel { // input let context: AppContext let authContext: AuthContext - let userFetchedResultsController: UserFetchedResultsController - +// let userFetchedResultsController: UserFetchedResultsController + @Published var records = [Mastodon.Entity.Account]() @Published var familiarFollowers: Mastodon.Entity.FamiliarFollowers? // output @@ -27,19 +27,19 @@ final class FamiliarFollowersViewModel { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalPredicate: nil - ) +// self.userFetchedResultsController = UserFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalPredicate: nil +// ) // end init $familiarFollowers - .map { familiarFollowers -> [MastodonUser.ID] in + .map { familiarFollowers in guard let familiarFollowers = familiarFollowers else { return [] } - return familiarFollowers.accounts.map { $0.id } + return familiarFollowers.accounts } - .assign(to: \.userIDs, on: userFetchedResultsController) + .assign(to: \.records, on: self) .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift index 367a4d51f..63112af79 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+Diffable.swift @@ -32,7 +32,7 @@ extension FavoriteViewModel { stateMachine.enter(State.Reloading.self) - statusFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift index a1a8d0f99..4bd9a3696 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift @@ -56,7 +56,7 @@ extension FavoriteViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.statusFetchedResultsController.statusIDs = [] + viewModel.records = [] stateMachine.enter(Loading.self) } @@ -127,10 +127,10 @@ extension FavoriteViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var newRecords = viewModel.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !newRecords.contains(where: { $0.id == status.id }) else { continue } + newRecords.append(status) hasNewStatusesAppend = true } @@ -146,7 +146,7 @@ extension FavoriteViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + viewModel.records = newRecords } catch { await enter(state: Fail.self) } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift index 0dd3c7203..f7f10ba61 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel.swift @@ -7,10 +7,9 @@ import UIKit import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonCore +import MastodonSDK final class FavoriteViewModel { @@ -19,9 +18,9 @@ final class FavoriteViewModel { // input let context: AppContext let authContext: AuthContext - let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() - + @Published var records = [Mastodon.Entity.Status]() + // output var diffableDataSource: UITableViewDiffableDataSource? private(set) lazy var stateMachine: GKStateMachine = { @@ -40,11 +39,6 @@ final class FavoriteViewModel { init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) } } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift index d0676dc59..c5bd662b8 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift @@ -28,7 +28,7 @@ extension FollowerListViewModel { snapshot.appendItems([.bottomLoader], toSection: .main) diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil) - userFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index fb7b20166..bc0a5cfd3 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -61,7 +61,7 @@ extension FollowerListViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.userFetchedResultsController.userIDs = [] + viewModel.records = [] stateMachine.enter(Loading.self) } @@ -139,10 +139,10 @@ extension FollowerListViewModel.State { ) var hasNewAppend = false - var userIDs = viewModel.userFetchedResultsController.userIDs + var newRecords = viewModel.records for user in response.value { - guard !userIDs.contains(user.id) else { continue } - userIDs.append(user.id) + guard !newRecords.contains(where: { $0.id == user.id }) else { continue } + newRecords.append(user) hasNewAppend = true } @@ -155,7 +155,7 @@ extension FollowerListViewModel.State { } self.maxID = maxID - viewModel.userFetchedResultsController.userIDs = userIDs + viewModel.records = newRecords } catch { await enter(state: Fail.self) diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift index ab1144e54..e2281bb25 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -19,7 +19,8 @@ final class FollowerListViewModel { // input let context: AppContext let authContext: AuthContext - let userFetchedResultsController: UserFetchedResultsController +// let userFetchedResultsController: UserFetchedResultsController + @Published var records = [Mastodon.Entity.Account]() let listBatchFetchViewModel = ListBatchFetchViewModel() @Published var domain: String? @@ -48,11 +49,11 @@ final class FollowerListViewModel { ) { self.context = context self.authContext = authContext - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: domain, - additionalPredicate: nil - ) +// self.userFetchedResultsController = UserFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: domain, +// additionalPredicate: nil +// ) self.domain = domain self.userID = userID // end init diff --git a/Mastodon/Scene/Profile/MeProfileViewModel.swift b/Mastodon/Scene/Profile/MeProfileViewModel.swift index 7f88d2ffe..2eb0248b7 100644 --- a/Mastodon/Scene/Profile/MeProfileViewModel.swift +++ b/Mastodon/Scene/Profile/MeProfileViewModel.swift @@ -15,11 +15,10 @@ import MastodonSDK final class MeProfileViewModel: ProfileViewModel { init(context: AppContext, authContext: AuthContext) { - let user = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) super.init( context: context, authContext: authContext, - optionalMastodonUser: user + optionalMastodonUser: authContext.mastodonAuthenticationBox.inMemoryCache.meAccount ) $me @@ -36,17 +35,7 @@ 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 - } + self.me = try await context.apiService.authenticatedUserInfo(authenticationBox: authContext.mastodonAuthenticationBox).value } catch { // do nothing? } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 012ebf61b..9d15fb09e 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import MastodonSDK import MastodonMeta import MastodonAsset @@ -33,8 +32,8 @@ class ProfileViewModel: NSObject { // input let context: AppContext let authContext: AuthContext - @Published var me: MastodonUser? - @Published var user: MastodonUser? + @Published var me: Mastodon.Entity.Account? + @Published var user: Mastodon.Entity.Account? let viewDidAppear = PassthroughSubject() @@ -56,7 +55,7 @@ class ProfileViewModel: NSObject { // @Published var protected: Bool? = nil // let needsPagePinToTop = CurrentValueSubject(false) - init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: MastodonUser?) { + init(context: AppContext, authContext: AuthContext, optionalMastodonUser mastodonUser: Mastodon.Entity.Account?) { self.context = context self.authContext = authContext self.user = mastodonUser @@ -82,7 +81,9 @@ class ProfileViewModel: NSObject { super.init() // bind me - self.me = authContext.mastodonAuthenticationBox.authentication.user(in: context.managedObjectContext) + relationshipViewModel.me = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount + + $me .assign(to: \.me, on: relationshipViewModel) .store(in: &disposeBag) @@ -91,7 +92,8 @@ class ProfileViewModel: NSObject { $user .map { user -> UserIdentifier? in guard let user = user else { return nil } - return MastodonUserIdentifier(domain: user.domain, userID: user.id) +#warning("fix domain!") + return MastodonUserIdentifier(domain: user.domain!, userID: user.id) } .assign(to: &$userIdentifier) $user @@ -122,14 +124,11 @@ class ProfileViewModel: NSObject { .store(in: &disposeBag) // query relationship - let userRecord = $user.map { user -> ManagedObjectRecord? in - user.flatMap { ManagedObjectRecord(objectID: $0.objectID) } - } let pendingRetryPublisher = CurrentValueSubject(1) // observe friendship Publishers.CombineLatest( - userRecord, + $user, pendingRetryPublisher ) .sink { [weak self] userRecord, _ in @@ -178,18 +177,17 @@ class ProfileViewModel: NSObject { // fetch profile info before edit func fetchEditProfileInfo() -> AnyPublisher, Error> { - guard let me = me, - let mastodonAuthentication = me.mastodonAuthentication + guard let me = me else { return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() } - let authorization = Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) - return context.apiService.accountVerifyCredentials(domain: me.domain, authorization: authorization) + let authorization = Mastodon.API.OAuth.Authorization(accessToken: authContext.mastodonAuthenticationBox.userAuthorization.accessToken) + return context.apiService.accountVerifyCredentials(domain: authContext.mastodonAuthenticationBox.domain, authorization: authorization) } private func updateRelationship( - record: ManagedObjectRecord, + record: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { let response = try await context.apiService.relationship( diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 832c25858..ab1f2593d 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -38,15 +38,7 @@ final class RemoteProfileViewModel: ProfileViewModel { } } 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.user = response.value } .store(in: &disposeBag) } @@ -59,33 +51,8 @@ final class RemoteProfileViewModel: ProfileViewModel { 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.user = response.value.account } // end Task } @@ -114,15 +81,7 @@ final class RemoteProfileViewModel: ProfileViewModel { } } 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 + self.user = value } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift index 67f2b8035..1002ed74d 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift @@ -48,7 +48,7 @@ extension UserTimelineViewModel { ).map { $0 || $1 || $2 } Publishers.CombineLatest( - statusFetchedResultsController.$records, + $records, needsTimelineHidden.removeDuplicates() ) .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift index cd0110a87..1d5b20ee6 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -56,7 +56,7 @@ extension UserTimelineViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.statusFetchedResultsController.statusIDs = [] + viewModel.records = [] stateMachine.enter(Loading.self) } @@ -112,7 +112,7 @@ extension UserTimelineViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - let maxID = viewModel.statusFetchedResultsController.statusIDs.last + let maxID = viewModel.records.last?.id guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else { stateMachine.enter(Fail.self) @@ -135,10 +135,10 @@ extension UserTimelineViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var newRecords = viewModel.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !newRecords.contains(where: { $0.id == status.id }) else { continue } + newRecords.append(status) hasNewStatusesAppend = true } @@ -147,7 +147,7 @@ extension UserTimelineViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + viewModel.records = newRecords } catch { await enter(state: Fail.self) diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index 0c8d634e5..3e6eb1549 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -21,7 +21,7 @@ final class UserTimelineViewModel { let context: AppContext let authContext: AuthContext let title: String - let statusFetchedResultsController: StatusFetchedResultsController +// let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @Published var userIdentifier: UserIdentifier? @Published var queryFilter: QueryFilter @@ -32,7 +32,8 @@ final class UserTimelineViewModel { // let userDisplayName = CurrentValueSubject(nil) // for suspended prompt label // var dataSourceDidUpdate = PassthroughSubject() - + @Published var records = [Mastodon.Entity.Status]() + // output var diffableDataSource: UITableViewDiffableDataSource? private(set) lazy var stateMachine: GKStateMachine = { @@ -57,11 +58,11 @@ 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( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalTweetPredicate: nil +// ) self.queryFilter = queryFilter } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift index d4830affc..22dc8d532 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+Diffable.swift @@ -33,7 +33,7 @@ extension UserListViewModel { // trigger initial loading stateMachine.enter(UserListViewModel.State.Reloading.self) - userFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift index cb6e9d3fa..77d2dbd61 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift @@ -55,7 +55,7 @@ extension UserListViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.userFetchedResultsController.userIDs = [] + viewModel.records = [] stateMachine.enter(Loading.self) } @@ -140,10 +140,10 @@ extension UserListViewModel.State { } var hasNewAppend = false - var userIDs = viewModel.userFetchedResultsController.userIDs + var newRecords = viewModel.records for user in response.value { - guard !userIDs.contains(user.id) else { continue } - userIDs.append(user.id) + guard !newRecords.contains(where: { $0.id == user.id }) else { continue } + newRecords.append(user) hasNewAppend = true } @@ -155,7 +155,7 @@ extension UserListViewModel.State { await enter(state: NoMore.self) } self.maxID = maxID - viewModel.userFetchedResultsController.userIDs = userIDs + viewModel.records = newRecords } catch { await enter(state: Fail.self) @@ -179,7 +179,8 @@ extension UserListViewModel.State { guard let viewModel = viewModel else { return } // trigger reload - viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs +// viewModel.userFetchedResultsController.userIDs = viewModel.userFetchedResultsController.userIDs + stateMachine?.enter(Loading.self) } } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift index d27562b94..e319d0639 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel.swift @@ -7,9 +7,9 @@ import UIKit import Combine -import CoreDataStack import GameplayKit import MastodonCore +import MastodonSDK final class UserListViewModel { var disposeBag = Set() @@ -18,9 +18,10 @@ final class UserListViewModel { let context: AppContext let authContext: AuthContext let kind: Kind - let userFetchedResultsController: UserFetchedResultsController +// let userFetchedResultsController: UserFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() - + @Published var records = [Mastodon.Entity.Account]() + // output var diffableDataSource: UITableViewDiffableDataSource! @MainActor private(set) lazy var stateMachine: GKStateMachine = { @@ -43,11 +44,11 @@ final class UserListViewModel { self.context = context self.authContext = authContext self.kind = kind - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalPredicate: nil - ) +// self.userFetchedResultsController = UserFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalPredicate: nil +// ) // end init } } @@ -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: Mastodon.Entity.Status) + case favoritedBy(status: Mastodon.Entity.Status) } } diff --git a/Mastodon/Scene/Report/Report/ReportViewModel.swift b/Mastodon/Scene/Report/Report/ReportViewModel.swift index cb840d213..8d50ab50a 100644 --- a/Mastodon/Scene/Report/Report/ReportViewModel.swift +++ b/Mastodon/Scene/Report/Report/ReportViewModel.swift @@ -6,8 +6,6 @@ // import Combine -import CoreData -import CoreDataStack import Foundation import GameplayKit import MastodonSDK @@ -28,8 +26,8 @@ class ReportViewModel { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord - let status: ManagedObjectRecord? + let user: Mastodon.Entity.Account + let status: Mastodon.Entity.Status? // output @Published var isReporting = false @@ -38,8 +36,8 @@ class ReportViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, - status: ManagedObjectRecord? + user: Mastodon.Entity.Account, + status: Mastodon.Entity.Status? ) { self.context = context self.authContext = authContext @@ -56,16 +54,7 @@ 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 - } + reportReasonViewModel.headline = L10n.Scene.Report.StepOne.whatsWrongWithThisUsername(user.acctWithDomain) } // end Task } @@ -95,64 +84,56 @@ 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: [Status.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 - } - 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 - } + // the status picker is essential step in report flow + // only check isSkip or not + let statusIDs: [Mastodon.Entity.Status.ID]? = { + if self.reportStatusViewModel.isSkip { + let _id: Mastodon.Entity.Status.ID? = self.reportStatusViewModel.status.flatMap { record -> Mastodon.Entity.Status.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 _id.flatMap { [$0] } ?? [] + } else { + return self.reportStatusViewModel.selectStatuses.compactMap { record -> Mastodon.Entity.Status.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 + } + }() + + let query = Mastodon.API.Reports.FileReportQuery( + accountID: user.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 { + case .violateRule: + let ruleIDs = self.reportServerRulesViewModel.selectRules.map { $0.id }.sorted() + return ruleIDs + default: return nil } }() - return Mastodon.API.Reports.FileReportQuery( - accountID: user.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 { - 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 diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift index de987b512..d01821203 100644 --- a/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift +++ b/Mastodon/Scene/Report/ReportResult/ReportResultViewModel.swift @@ -6,8 +6,6 @@ // import Combine -import CoreData -import CoreDataStack import Foundation import MastodonSDK import UIKit @@ -23,7 +21,7 @@ class ReportResultViewModel: ObservableObject { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let user: Mastodon.Entity.Account let isReported: Bool var headline: String { @@ -48,7 +46,7 @@ class ReportResultViewModel: ObservableObject { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, + user: Mastodon.Entity.Account, isReported: Bool ) { self.context = context @@ -58,10 +56,8 @@ class ReportResultViewModel: ObservableObject { // 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.relationshipViewModel.me = authContext.mastodonAuthenticationBox.inMemoryCache.meAccount self.avatarURL = user.avatarImageURL() self.username = user.acctWithDomain diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift index fce56a2b9..8d99eaa00 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+Diffable.swift @@ -32,7 +32,7 @@ extension ReportStatusViewModel { snapshot.appendSections([.main]) diffableDataSource?.apply(snapshot) - statusFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift index d4fb507b2..03cefe603 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel+State.swift @@ -7,9 +7,8 @@ import func QuartzCore.CACurrentMediaTime import Foundation -import CoreData -import CoreDataStack import GameplayKit +import MastodonSDK extension ReportStatusViewModel { class State: GKState { @@ -64,14 +63,11 @@ extension ReportStatusViewModel.State { super.didEnter(from: previousState) guard let viewModel else { return } - let maxID = viewModel.statusFetchedResultsController.statusIDs.last + let maxID = viewModel.records.last?.id Task { 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 - } + let _userID: Mastodon.Entity.Account.ID? = viewModel.user.id guard let userID = _userID else { await enter(state: Fail.self) return @@ -89,10 +85,10 @@ extension ReportStatusViewModel.State { ) var hasNewStatusesAppend = false - var statusIDs = viewModel.statusFetchedResultsController.statusIDs + var newRecords = viewModel.records for status in response.value { - guard !statusIDs.contains(status.id) else { continue } - statusIDs.append(status.id) + guard !newRecords.contains(where: { $0.id == status.id }) else { continue } + newRecords.append(status) hasNewStatusesAppend = true } @@ -101,7 +97,7 @@ extension ReportStatusViewModel.State { } else { await enter(state: NoMore.self) } - viewModel.statusFetchedResultsController.statusIDs = statusIDs + viewModel.records = newRecords } catch { await enter(state: Fail.self) diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift index 8c41e1ce0..8e8fad768 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewModel.swift @@ -6,8 +6,6 @@ // import Combine -import CoreData -import CoreDataStack import Foundation import GameplayKit import MastodonSDK @@ -24,14 +22,15 @@ class ReportStatusViewModel { // input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord - let status: ManagedObjectRecord? - let statusFetchedResultsController: StatusFetchedResultsController + let user: Mastodon.Entity.Account + let status: Mastodon.Entity.Status? +// let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @Published var isSkip = false - @Published var selectStatuses = OrderedSet>() - + @Published var selectStatuses = OrderedSet() + @Published var records = [Mastodon.Entity.Status]() + // output var diffableDataSource: UITableViewDiffableDataSource? private(set) lazy var stateMachine: GKStateMachine = { @@ -51,18 +50,18 @@ class ReportStatusViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord, - status: ManagedObjectRecord? + user: Mastodon.Entity.Account, + status: Mastodon.Entity.Status? ) { 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( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalTweetPredicate: nil +// ) // end init if let status = status { diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift index a4239bbc4..d329bdc38 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewModel.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import MastodonCore import MastodonSDK @@ -18,7 +17,7 @@ class ReportSupplementaryViewModel { // Input let context: AppContext let authContext: AuthContext - let user: ManagedObjectRecord + let user: Mastodon.Entity.Account let commentContext = ReportItem.CommentContext() @Published var isSkip = false @@ -31,7 +30,7 @@ class ReportSupplementaryViewModel { init( context: AppContext, authContext: AuthContext, - user: ManagedObjectRecord + user: Mastodon.Entity.Account ) { self.context = context self.authContext = authContext diff --git a/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Report/Share/Cell/ReportStatusTableViewCell+ViewModel.swift index 00d079cfa..61021e67a 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: Mastodon.Entity.Status - init(value: Status) { + init(value: Mastodon.Entity.Status) { self.value = value } } diff --git a/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift b/Mastodon/Scene/Search/SearchDetail/Search Results Overview/SearchResultOverviewCoordinator.swift index 66ec904f7..b4e999b0b 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: status ) } else if let url = URL(string: urlString) { let prefixedURL: URL? @@ -111,27 +99,12 @@ extension SearchResultOverviewCoordinator: SearchResultsOverviewTableViewControl } func showProfile(_ viewController: SearchResultsOverviewTableViewController, for account: Mastodon.Entity.Account) { - let managedObjectContext = context.managedObjectContext - let domain = authContext.mastodonAuthenticationBox.domain - Task { - let user = try await managedObjectContext.perform { - return Persistence.MastodonUser.fetch(in: managedObjectContext, - context: Persistence.MastodonUser.PersistContext( - domain: domain, - entity: account, - cache: nil, - networkDate: Date() - )) - } + await DataSourceFacade.coordinateToProfileScene(provider: viewController, + user: account) - if let user { - await DataSourceFacade.coordinateToProfileScene(provider: viewController, - user: user.asRecord) - - await DataSourceFacade.responseToCreateSearchHistory(provider: viewController, - item: .user(record: user.asRecord)) - } + await DataSourceFacade.responseToCreateSearchHistory(provider: viewController, + item: .user(record: account)) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryItem.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryItem.swift index ae156a81f..da08abb3a 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryItem.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryItem.swift @@ -6,10 +6,9 @@ // import Foundation -import CoreData -import CoreDataStack +import MastodonSDK enum SearchHistoryItem: Hashable { - case hashtag(ManagedObjectRecord) - case user(ManagedObjectRecord) + case hashtag(Mastodon.Entity.Tag) + case user(Mastodon.Entity.Account) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift index a1bae2638..3c243055c 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewController+DataSourceProvider.swift @@ -24,7 +24,7 @@ extension SearchHistoryViewController: DataSourceProvider { case .user(let record): return .user(record: record) case .hashtag(let record): - return .hashtag(tag: .record(record)) + return .hashtag(tag: .entity(record)) } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift index a8af8e845..a80091c85 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel+Diffable.swift @@ -27,7 +27,7 @@ extension SearchHistoryViewModel { snapshot.appendSections([.main]) diffableDataSource?.apply(snapshot, animatingDifferences: false) - searchHistoryFetchedResultController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } @@ -35,23 +35,24 @@ extension SearchHistoryViewModel { Task { do { - let managedObjectContext = self.context.managedObjectContext - let items: [SearchHistoryItem] = try await managedObjectContext.perform { - var items: [SearchHistoryItem] = [] +// let managedObjectContext = self.context.managedObjectContext +// let items: [SearchHistoryItem] = try await managedObjectContext.perform { +// var items: [SearchHistoryItem] = [] +// +// for record in records { +// guard let searchHistory = record.object(in: managedObjectContext) else { continue } +// if let user = searchHistory.account { +// items.append(.user(.init(objectID: user.objectID))) +// } else if let hashtag = searchHistory.hashtag { +// items.append(.hashtag(.init(objectID: hashtag.objectID))) +// } +// } +// +// return items +// } - for record in records { - guard let searchHistory = record.object(in: managedObjectContext) else { continue } - if let user = searchHistory.account { - items.append(.user(.init(objectID: user.objectID))) - } else if let hashtag = searchHistory.hashtag { - items.append(.hashtag(.init(objectID: hashtag.objectID))) - } - } - - return items - } - - let mostRecentItems = Array(items.prefix(10)) + #warning("Reimplement storing and recovering search history") + let mostRecentItems = [SearchHistoryItem]() //Array(items.prefix(10)) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(mostRecentItems, toSection: .main) diff --git a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift index d1360efc4..575bb8c67 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchHistory/SearchHistoryViewModel.swift @@ -7,8 +7,12 @@ import UIKit import Combine -import CoreDataStack import MastodonCore +import MastodonSDK + +struct SearchHistoryQueryItem { + +} final class SearchHistoryViewModel { var disposeBag = Set() @@ -16,18 +20,13 @@ final class SearchHistoryViewModel { // input let context: AppContext let authContext: AuthContext - let searchHistoryFetchedResultController: SearchHistoryFetchedResultController - + @Published var records = [SearchHistoryQueryItem]() // output var diffableDataSource: UICollectionViewDiffableDataSource? init(context: AppContext, authContext: AuthContext) { self.context = context self.authContext = authContext - self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext) - - searchHistoryFetchedResultController.domain.value = authContext.mastodonAuthenticationBox.domain - searchHistoryFetchedResultController.userID.value = authContext.mastodonAuthenticationBox.userID } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift index 813836925..36c2c1c8c 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultItem.swift @@ -6,13 +6,11 @@ // import Foundation -import CoreData -import CoreDataStack import MastodonSDK enum SearchResultItem: Hashable { - case user(ManagedObjectRecord) - case status(ManagedObjectRecord) + case user(Mastodon.Entity.Account) + case status(Mastodon.Entity.Status) case hashtag(tag: Mastodon.Entity.Tag) case bottomLoader(attribute: BottomLoaderAttribute) } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift index 5b74ba8aa..b25d41130 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+Diffable.swift @@ -32,8 +32,8 @@ extension SearchResultViewModel { diffableDataSource.apply(snapshot, animatingDifferences: false) Publishers.CombineLatest3( - statusFetchedResultsController.$records, - userFetchedResultsController.$records, + $statusRecords, + $userRecords, $hashtags ) .map { statusRecords, userRecords, hashtags in diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift index c3ddc2f0a..0e159c74a 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel.swift @@ -7,8 +7,6 @@ import Foundation import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonSDK import MastodonCore @@ -22,8 +20,11 @@ final class SearchResultViewModel { let searchScope: SearchScope let searchText: String @Published var hashtags: [Mastodon.Entity.Tag] = [] - let userFetchedResultsController: UserFetchedResultsController - let statusFetchedResultsController: StatusFetchedResultsController + @Published var userRecords = [Mastodon.Entity.Account]() + @Published var statusRecords = [Mastodon.Entity.Status]() + +// let userFetchedResultsController: UserFetchedResultsController +// let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() var cellFrameCache = NSCache() @@ -51,15 +52,15 @@ final class SearchResultViewModel { self.searchScope = searchScope self.searchText = searchText - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalPredicate: nil - ) - self.statusFetchedResultsController = StatusFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: authContext.mastodonAuthenticationBox.domain, - additionalTweetPredicate: nil - ) +// self.userFetchedResultsController = UserFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalPredicate: nil +// ) +// self.statusFetchedResultsController = StatusFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: authContext.mastodonAuthenticationBox.domain, +// additionalTweetPredicate: nil +// ) } } diff --git a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift index 576d2eadf..737ec0e97 100644 --- a/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/NotificationView+Configuration.swift @@ -8,17 +8,16 @@ import UIKit import Combine import MastodonUI -import CoreDataStack import MetaTextKit import MastodonMeta import Meta import MastodonAsset import MastodonCore import MastodonLocalization -import class CoreDataStack.Notification +import MastodonSDK extension NotificationView { - public func configure(feed: Feed) { + public func configure(feed: FeedItem) { guard let notification = feed.notification else { assertionFailure() return @@ -29,17 +28,10 @@ extension NotificationView { } extension NotificationView { - public func configure(notification: Notification) { - viewModel.objects.insert(notification) - + public func configure(notification: Mastodon.Entity.Notification) { configureAuthor(notification: notification) - - guard let type = MastodonNotificationType(rawValue: notification.typeRaw) else { - assertionFailure() - return - } - - switch type { + + switch notification.type { case .follow: setAuthorContainerBottomPaddingViewDisplay() case .followRequest: @@ -63,167 +55,125 @@ extension NotificationView { } extension NotificationView { - private func configureAuthor(notification: Notification) { + private func configureAuthor(notification: Mastodon.Entity.Notification) { 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 + viewModel.authorName = { do { - let content = MastodonContent(content: author.displayNameWithFallback, emojis: emojis.asDictionary) + let content = MastodonContent(content: author.displayNameWithFallback, emojis: author.emojis?.asDictionary ?? [:]) 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) + viewModel.authorUsername = author.acct + // timestamp - viewModel.timestamp = notification.createAt + viewModel.timestamp = notification.createdAt - viewModel.visibility = notification.status?.visibility ?? ._other("") + viewModel.visibility = notification.status?.mastodonVisibility ?? ._other("") // notification type indicator - Publishers.CombineLatest3( - notification.publisher(for: \.typeRaw), - author.publisher(for: \.displayName), - author.publisher(for: \.emojis) - ) - .sink { [weak self] typeRaw, _, emojis in - guard let self = self else { return } - guard let type = MastodonNotificationType(rawValue: typeRaw) else { - self.viewModel.notificationIndicatorText = nil - return - } - self.viewModel.type = type + self.viewModel.type = notification.type - func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent { - let content = MastodonContent(content: text, emojis: emojis) - guard let metaContent = try? MastodonMetaContent.convert(document: content) else { - return PlaintextMetaContent(string: text) - } - 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 - ) - case .followRequest: - self.viewModel.notificationIndicatorText = createMetaContent( - text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou, - emojis: emojis.asDictionary - ) - case .mention: - self.viewModel.notificationIndicatorText = createMetaContent( - text: L10n.Scene.Notification.NotificationDescription.mentionedYou, - emojis: emojis.asDictionary - ) - case .reblog: - self.viewModel.notificationIndicatorText = createMetaContent( - text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost, - emojis: emojis.asDictionary - ) - case .favourite: - self.viewModel.notificationIndicatorText = createMetaContent( - text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost, - emojis: emojis.asDictionary - ) - case .poll: - self.viewModel.notificationIndicatorText = createMetaContent( - text: L10n.Scene.Notification.NotificationDescription.pollHasEnded, - emojis: emojis.asDictionary - ) - case .status: - self.viewModel.notificationIndicatorText = createMetaContent( - text: .empty, - emojis: emojis.asDictionary - ) - case ._other: - self.viewModel.notificationIndicatorText = nil + func createMetaContent(text: String, emojis: MastodonContent.Emojis) -> MetaContent { + let content = MastodonContent(content: text, emojis: emojis) + guard let metaContent = try? MastodonMetaContent.convert(document: content) else { + return PlaintextMetaContent(string: text) } + return metaContent } - .store(in: &disposeBag) + + // TODO: fix the i18n. The subject should assert place at the string beginning + guard let type = viewModel.type else { return } - 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 - }) + switch type { + case .follow: + self.viewModel.notificationIndicatorText = createMetaContent( + text: L10n.Scene.Notification.NotificationDescription.followedYou, + emojis: author.emojis?.asDictionary ?? [:] + ) + case .followRequest: + self.viewModel.notificationIndicatorText = createMetaContent( + text: L10n.Scene.Notification.NotificationDescription.requestToFollowYou, + emojis: author.emojis?.asDictionary ?? [:] + ) + case .mention: + self.viewModel.notificationIndicatorText = createMetaContent( + text: L10n.Scene.Notification.NotificationDescription.mentionedYou, + emojis: author.emojis?.asDictionary ?? [:] + ) + case .reblog: + self.viewModel.notificationIndicatorText = createMetaContent( + text: L10n.Scene.Notification.NotificationDescription.rebloggedYourPost, + emojis: author.emojis?.asDictionary ?? [:] + ) + case .favourite: + self.viewModel.notificationIndicatorText = createMetaContent( + text: L10n.Scene.Notification.NotificationDescription.favoritedYourPost, + emojis: author.emojis?.asDictionary ?? [:] + ) + case .poll: + self.viewModel.notificationIndicatorText = createMetaContent( + text: L10n.Scene.Notification.NotificationDescription.pollHasEnded, + emojis: author.emojis?.asDictionary ?? [:] + ) + case .status: + self.viewModel.notificationIndicatorText = createMetaContent( + text: .empty, + emojis: author.emojis?.asDictionary ?? [:] + ) + case ._other: + self.viewModel.notificationIndicatorText = nil + } + + guard let authContext = viewModel.authContext else { return } + + Task { + guard let context = viewModel.context else { return } + if let relationship = try await context.apiService.relationship(records: [author], authenticationBox: authContext.mastodonAuthenticationBox).value.first { + + viewModel.isMuting = relationship.muting == true + viewModel.isBlocking = relationship.blockedBy == true // OR: blocking ??? + viewModel.isFollowed = relationship.followedBy } - .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) + +// let pendingFollowRequests = try await context.apiService.pendingFollowRequest(userID: notification.account.id, authenticationBox: authContext.mastodonAuthenticationBox).value + +// pendingFollowRequests + } // 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) + viewModel.isMyself = (author.domain == authContext.mastodonAuthenticationBox.domain) && (author.id == authContext.mastodonAuthenticationBox.userID) + #warning("re-implemented the two below") // 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) +// notification.publisher(for: \.followRequestState) +// .assign(to: \.followRequestState, on: viewModel) +// .store(in: &disposeBag) + +// notification.publisher(for: \.transientFollowRequestState) +// .assign(to: \.transientFollowRequestState, on: viewModel) +// .store(in: &disposeBag) // 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) +// 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/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift index 631e5b337..73b513758 100644 --- a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift @@ -7,94 +7,82 @@ import UIKit import Combine -import CoreDataStack import MetaTextKit import MastodonCore import MastodonUI import MastodonSDK extension PollOptionView { - public func configure(pollOption option: PollOption) { - guard let poll = option.poll, let status = poll.status else { - assertionFailure("PollOption to be configured is expected to be part of Poll with Status") - return - } + public func configure(status: Mastodon.Entity.Status, pollOption option: Mastodon.Entity.Poll.Option, poll: Mastodon.Entity.Poll) { - viewModel.objects.insert(option) - // metaContent - option.publisher(for: \.title) - .map { title -> MetaContent? in - return PlaintextMetaContent(string: title) - } - .assign(to: \.metaContent, on: viewModel) - .store(in: &disposeBag) + viewModel.metaContent = PlaintextMetaContent(string: option.title) + // percentage - Publishers.CombineLatest( - poll.publisher(for: \.votersCount), - option.publisher(for: \.votesCount) - ) - .map { pollVotersCount, optionVotesCount -> Double? in + viewModel.percentage = { + let pollVotersCount = poll.votersCount ?? 0 + let optionVotesCount = option.votesCount ?? 0 guard pollVotersCount > 0, optionVotesCount >= 0 else { return 0 } return Double(optionVotesCount) / Double(pollVotersCount) - } - .assign(to: \.percentage, on: viewModel) - .store(in: &disposeBag) + }() + // $isExpire - poll.publisher(for: \.expired) - .assign(to: \.isExpire, on: viewModel) - .store(in: &disposeBag) + viewModel.isExpire = poll.expired + // isMultiple viewModel.isMultiple = poll.multiple - let optionIndex = option.index - let authorDomain = status.author.domain - let authorID = status.author.id +// let optionIndex = option.index + let authorDomain = status.account.domain + let authorID = status.account.id + + #warning("re-implemented the code below") // isSelect, isPollVoted, isMyPoll - Publishers.CombineLatest4( - option.publisher(for: \.poll), - option.publisher(for: \.votedBy), - option.publisher(for: \.isSelected), - viewModel.$authContext - ) - .sink { [weak self] poll, optionVotedBy, isSelected, authContext in - guard let self = self, let poll = poll else { return } - - let domain = authContext?.mastodonAuthenticationBox.domain ?? "" - let userID = authContext?.mastodonAuthenticationBox.userID ?? "" - - let options = poll.options - let pollVoteBy = poll.votedBy ?? Set() - - let isMyPoll = authorDomain == domain - && authorID == userID - - let votedOptions = options.filter { option in - let votedBy = option.votedBy ?? Set() - return votedBy.contains(where: { $0.id == userID && $0.domain == domain }) - } - let isRemoteVotedOption = votedOptions.contains(where: { $0.index == optionIndex }) - let isRemoteVotedPoll = pollVoteBy.contains(where: { $0.id == userID && $0.domain == domain }) - - let isLocalVotedOption = isSelected - - let isSelect: Bool? = { - if isLocalVotedOption { - return true - } else if !votedOptions.isEmpty { - return isRemoteVotedOption ? true : false - } else if isRemoteVotedPoll, votedOptions.isEmpty { - // the poll voted. But server not mark voted options - return nil - } else { - return false - } - }() - self.viewModel.isSelect = isSelect - self.viewModel.isPollVoted = isRemoteVotedPoll - self.viewModel.isMyPoll = isMyPoll - } - .store(in: &disposeBag) +// Publishers.CombineLatest4( +// option.publisher(for: \.poll), +// option.publisher(for: \.votedBy), +// option.publisher(for: \.isSelected), +// viewModel.$authContext +// ) +// .sink { [weak self] poll, optionVotedBy, isSelected, authContext in +// guard let self = self, let poll = poll else { return } +// +// let domain = authContext?.mastodonAuthenticationBox.domain ?? "" +// let userID = authContext?.mastodonAuthenticationBox.userID ?? "" +// +// let options = poll.options +// let pollVoteBy = poll.votedBy ?? Set() +// +// let isMyPoll = authorDomain == domain +// && authorID == userID +// +// let votedOptions = options.filter { option in +// let votedBy = option.votedBy ?? Set() +// return votedBy.contains(where: { $0.id == userID && $0.domain == domain }) +// } +// let isRemoteVotedOption = votedOptions.contains(where: { $0.index == optionIndex }) +// let isRemoteVotedPoll = pollVoteBy.contains(where: { $0.id == userID && $0.domain == domain }) +// +// let isLocalVotedOption = isSelected +// +// let isSelect: Bool? = { +// if isLocalVotedOption { +// return true +// } else if !votedOptions.isEmpty { +// return isRemoteVotedOption ? true : false +// } else if isRemoteVotedPoll, votedOptions.isEmpty { +// // the poll voted. But server not mark voted options +// return nil +// } else { +// return false +// } +// }() +// self.viewModel.isSelect = isSelect +// self.viewModel.isPollVoted = isRemoteVotedPoll +// self.viewModel.isMyPoll = isMyPoll +// } +// .store(in: &disposeBag) + // appearance checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in return trailtCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor diff --git a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift index e3f91f462..3dae6045e 100644 --- a/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift +++ b/Mastodon/Scene/Share/View/Content/UserView+Configuration.swift @@ -8,7 +8,6 @@ import UIKit import Combine import MastodonUI -import CoreDataStack import MastodonLocalization import MastodonMeta import MastodonCore @@ -17,55 +16,32 @@ import MastodonSDK import MastodonAsset extension UserView { - public func configure(user: MastodonUser, delegate: UserViewDelegate?) { + public func configure(user: Mastodon.Entity.Account, 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) + viewModel.authorAvatarImageURL = user.avatarImageURL() // author name - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _, emojis in + viewModel.authorName = { do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: emojis.asDictionary) + let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.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) + viewModel.authorUsername = user.acct - user.publisher(for: \.followersCount) - .map { Int($0) } - .assign(to: \.authorFollowers, on: viewModel) - .store(in: &disposeBag) + viewModel.authorFollowers = user.followersCount - 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) + viewModel.authorVerifiedLink = user.fields?.first(where: { $0.verifiedAt != nil })?.value } func configure(with account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, delegate: UserViewDelegate?) { diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCell+ViewModel.swift index c3455fd0e..5a7b0674a 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(FeedItem) + case status(Mastodon.Entity.Status) } } } @@ -38,13 +38,8 @@ 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..828c40a89 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(Mastodon.Entity.Status) } } } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift index 6b8613292..a6868243f 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell+ViewModel.swift @@ -6,7 +6,6 @@ // import UIKit -import CoreDataStack import MastodonUI import Combine import MastodonCore @@ -14,13 +13,13 @@ import MastodonSDK extension UserTableViewCell { final class ViewModel { - let user: MastodonUser + let user: 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>) { + init(user: Mastodon.Entity.Account, followedUsers: AnyPublisher<[String], Never>, blockedUsers: AnyPublisher<[String], Never>, followRequestedUsers: AnyPublisher<[String], Never>) { self.user = user self.followedUsers = followedUsers self.followRequestedUsers = followRequestedUsers @@ -32,7 +31,7 @@ extension UserTableViewCell { extension UserTableViewCell { func configure( - me: MastodonUser, + me: Mastodon.Entity.Account, tableView: UITableView, account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?, @@ -47,61 +46,62 @@ extension UserTableViewCell { } func configure( - me: MastodonUser? = nil, + me: Mastodon.Entity.Account? = nil, tableView: UITableView, viewModel: ViewModel, delegate: UserTableViewCellDelegate? ) { userView.configure(user: viewModel.user, delegate: delegate) - guard let me = me else { + guard let 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) + + #warning("re-implement the code below") +// 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) { + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account) { Task { try await DataSourceFacade.responseToUserViewButtonAction( dependency: self, - user: user.asRecord, + user: user, 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) } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift index 0f316bad8..79083ebd4 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift @@ -7,7 +7,6 @@ import UIKit import Combine -import CoreDataStack import MastodonAsset import MastodonLocalization import MastodonUI diff --git a/Mastodon/Scene/SuggestionAccount/RecommendAccountItem.swift b/Mastodon/Scene/SuggestionAccount/RecommendAccountItem.swift index 998f2f3e9..85fe74878 100644 --- a/Mastodon/Scene/SuggestionAccount/RecommendAccountItem.swift +++ b/Mastodon/Scene/SuggestionAccount/RecommendAccountItem.swift @@ -6,8 +6,8 @@ // import Foundation -import CoreDataStack +import MastodonSDK enum RecommendAccountItem: Hashable { - case account(ManagedObjectRecord) + case account(Mastodon.Entity.Account) } diff --git a/Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift b/Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift index c3f477a96..2f41ff5a7 100644 --- a/Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift +++ b/Mastodon/Scene/SuggestionAccount/RecommendAccountSection.swift @@ -36,16 +36,13 @@ extension RecommendAccountSection { switch item { case .account(let record): cell.delegate = configuration.suggestionAccountTableViewCellDelegate - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - cell.configure(viewModel: - SuggestionAccountTableViewCell.ViewModel( - user: user, - followedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds, - blockedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds, - followRequestedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs) - ) - } + cell.configure(viewModel: + SuggestionAccountTableViewCell.ViewModel( + user: record, + followedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followingUserIds, + blockedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.blockedUserIds, + followRequestedUsers: configuration.authContext.mastodonAuthenticationBox.inMemoryCache.followRequestedUserIDs) + ) } return cell } diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 35e1f929d..f74dd1509 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -6,14 +6,13 @@ // import Combine -import CoreData -import CoreDataStack import Foundation import UIKit import MastodonAsset import MastodonCore import MastodonUI import MastodonLocalization +import MastodonSDK class SuggestionAccountViewController: UIViewController, NeedsDependency { @@ -89,10 +88,9 @@ extension SuggestionAccountViewController: UITableViewDelegate { guard let item = tableViewDiffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { case .account(let record): - guard let account = record.object(in: context.managedObjectContext) else { return } - let cachedProfileViewModel = CachedProfileViewModel(context: context, authContext: viewModel.authContext, mastodonUser: account) + let profileViewModel = ProfileViewModel(context: context, authContext: viewModel.authContext, optionalMastodonUser: record) _ = coordinator.present( - scene: .profile(viewModel: cachedProfileViewModel), + scene: .profile(viewModel: profileViewModel), from: self, transition: .show ) @@ -104,7 +102,7 @@ extension SuggestionAccountViewController: UITableViewDelegate { return nil } - footerView.followAllButton.isEnabled = viewModel.userFetchedResultsController.records.isNotEmpty + footerView.followAllButton.isEnabled = viewModel.records.isNotEmpty footerView.delegate = self return footerView diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift index 81238105b..64eff383e 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewModel.swift @@ -6,8 +6,6 @@ // import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonSDK import MastodonCore @@ -25,53 +23,55 @@ final class SuggestionAccountViewModel: NSObject { // input let context: AppContext let authContext: AuthContext - let userFetchedResultsController: UserFetchedResultsController +// let userFetchedResultsController: UserFetchedResultsController var viewWillAppear = PassthroughSubject() // output var tableViewDiffableDataSource: UITableViewDiffableDataSource? + @Published var records = [Mastodon.Entity.Account]() + init( context: AppContext, authContext: AuthContext ) { self.context = context self.authContext = authContext - self.userFetchedResultsController = UserFetchedResultsController( - managedObjectContext: context.managedObjectContext, - domain: nil, - additionalPredicate: nil - ) +// self.userFetchedResultsController = UserFetchedResultsController( +// managedObjectContext: context.managedObjectContext, +// domain: nil, +// additionalPredicate: nil +// ) super.init() - userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain +// userFetchedResultsController.domain = authContext.mastodonAuthenticationBox.domain // fetch recommended users Task { - var userIDs: [MastodonUser.ID] = [] + var users: [Mastodon.Entity.Account] = [] do { let response = try await context.apiService.suggestionAccountV2( query: .init(limit: 5), authenticationBox: authContext.mastodonAuthenticationBox ) - userIDs = response.value.map { $0.account.id } + users = response.value.map { $0.account } } catch let error as Mastodon.API.Error where error.httpResponseStatus == .notFound { let response = try await context.apiService.suggestionAccount( query: nil, authenticationBox: authContext.mastodonAuthenticationBox ) - userIDs = response.value.map { $0.id } + users = response.value.map { $0 } } catch { } - guard userIDs.isNotEmpty else { return } - userFetchedResultsController.userIDs = userIDs + guard users.isNotEmpty else { return } + records = users } // fetch relationship - userFetchedResultsController.$records + $records .removeDuplicates() .sink { [weak self] records in guard let _ = self else { return } @@ -98,7 +98,7 @@ final class SuggestionAccountViewModel: NSObject { ) ) - userFetchedResultsController.$records + $records .receive(on: DispatchQueue.main) .sink { [weak self] records in guard let self = self else { return } @@ -116,13 +116,9 @@ final class SuggestionAccountViewModel: NSObject { func followAllSuggestedAccounts(_ dependency: NeedsDependency & AuthContextProvider, completion: (() -> Void)? = nil) { - let userRecords = userFetchedResultsController.records.compactMap { - $0.object(in: dependency.context.managedObjectContext)?.asRecord - } - Task { await withTaskGroup(of: Void.self, body: { taskGroup in - for user in userRecords { + for user in records { taskGroup.addTask { try? await DataSourceFacade.responseToUserViewButtonAction( dependency: dependency, diff --git a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift index 1b4fcb34c..774a18189 100644 --- a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift +++ b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell+ViewModel.swift @@ -2,17 +2,17 @@ import Combine import MastodonUI -import CoreDataStack +import MastodonSDK extension SuggestionAccountTableViewCell { final class ViewModel { - let user: MastodonUser + let user: Mastodon.Entity.Account let followedUsers: [String] let blockedUsers: [String] let followRequestedUsers: [String] - init(user: MastodonUser, followedUsers: [String], blockedUsers: [String], followRequestedUsers: [String]) { + init(user: Mastodon.Entity.Account, followedUsers: [String], blockedUsers: [String], followRequestedUsers: [String]) { self.user = user self.followedUsers = followedUsers self.followRequestedUsers = followRequestedUsers diff --git a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift index 1448be17f..08d72bdab 100644 --- a/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift +++ b/Mastodon/Scene/SuggestionAccount/TableView-Components/SuggestionAccountTableViewCell.swift @@ -102,11 +102,11 @@ final class SuggestionAccountTableViewCell: UITableViewCell { let metaContent: MetaContent = { do { - let mastodonContent = MastodonContent(content: viewModel.user.note ?? "", emojis: viewModel.user.emojis.asDictionary) + let mastodonContent = MastodonContent(content: viewModel.user.note, emojis: viewModel.user.emojis?.asDictionary ?? [:]) return try MastodonMetaContent.convert(document: mastodonContent) } catch { assertionFailure() - return PlaintextMetaContent(string: viewModel.user.note ?? "") + return PlaintextMetaContent(string: viewModel.user.note) } }() diff --git a/Mastodon/Scene/Thread/CachedThreadViewModel.swift b/Mastodon/Scene/Thread/CachedThreadViewModel.swift deleted file mode 100644 index 00c29e157..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 CoreDataStack -import MastodonCore - -final class CachedThreadViewModel: ThreadViewModel { - init(context: AppContext, authContext: AuthContext, status: Status) { - let threadContext = StatusItem.Thread.Context(status: .init(objectID: status.objectID)) - super.init( - context: context, - authContext: authContext, - optionalRoot: .root(context: threadContext) - ) - } -} diff --git a/Mastodon/Scene/Thread/Edit History/StatusEditHistoryTableViewCell.swift b/Mastodon/Scene/Thread/Edit History/StatusEditHistoryTableViewCell.swift index 3485cfeab..c568abb43 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: Mastodon.Entity.Status, 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..807b54329 100644 --- a/Mastodon/Scene/Thread/Edit History/StatusEditHistoryViewModel.swift +++ b/Mastodon/Scene/Thread/Edit History/StatusEditHistoryViewModel.swift @@ -1,14 +1,13 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import Foundation -import CoreDataStack import MastodonCore import MastodonUI import UIKit import MastodonSDK struct StatusEditHistoryViewModel { - let status: Status + let status: Mastodon.Entity.Status let edits: [Mastodon.Entity.StatusEdit] let appContext: AppContext diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift index 696e10492..dfdaa8143 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -28,17 +28,9 @@ final class RemoteThreadViewModel: ThreadViewModel { let response = try await context.apiService.status( statusID: statusID, authenticationBox: authContext.mastodonAuthenticationBox - ) + ).value - 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: response) 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: 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..3137b8929 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonCore import MastodonUI import MastodonSDK @@ -38,8 +36,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.inReplyToID != nil { snapshot.appendItems([.topLoader], toSection: .main) } @@ -81,8 +78,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.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..1e032f9b2 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel.swift @@ -7,8 +7,6 @@ import UIKit import Combine -import CoreData -import CoreDataStack import GameplayKit import MastodonSDK import MastodonMeta @@ -55,40 +53,26 @@ 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 +#warning("fix domain!") self.threadContext = .init( - domain: status.domain, + domain: status.account.domain!, statusID: status.id, replyToID: status.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.account.displayNameWithFallback) + let content = MastodonContent(content: title, emojis: status.account.emojis?.asDictionary ?? [:]) return try? MastodonMetaContent.convert(document: content) }() } @@ -116,16 +100,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/CoreDataStack/Entity/App/Feed.swift b/MastodonSDK/Sources/CoreDataStack/Entity/App/Feed.swift index 5fca61153..bbc764b7a 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/App/Feed.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/App/Feed.swift @@ -43,7 +43,7 @@ final public class Feed: NSManagedObject { @NSManaged public private(set) var updatedAt: Date // one-to-one relationship - @NSManaged public private(set) var status: Status? + @NSManaged public private(set) var status: StatusLegacy? @NSManaged public private(set) var notification: Notification? } @@ -97,17 +97,17 @@ extension Feed { 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 } - ) - ]) - } +// 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 } +// ) +// ]) +// } } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Application.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Application.swift index b40b2e5b2..5c9531da5 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Application.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Application.swift @@ -18,7 +18,7 @@ public final class Application: NSManagedObject { @NSManaged public private(set) var vapidKey: String? // one-to-one relationship - @NSManaged public private(set) var status: Status + @NSManaged public private(set) var status: StatusLegacy } public extension Application { diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift index 64f5e2120..9a22aaf46 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Card.swift @@ -53,7 +53,7 @@ public final class Card: NSManagedObject { @NSManaged public private(set) var html: String? // sourcery: autoGenerateRelationship - @NSManaged public private(set) var status: Status + @NSManaged public private(set) var status: StatusLegacy } extension Card { @@ -163,10 +163,10 @@ extension Card: AutoGenerateRelationship { // Generated using Sourcery // DO NOT EDIT public struct Relationship { - public let status: Status + public let status: StatusLegacy public init( - status: Status + status: StatusLegacy ) { self.status = status } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Emoji.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Emoji.swift index e9ee9d235..d7f8186e9 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Emoji.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Emoji.swift @@ -20,7 +20,7 @@ public final class Emoji: NSManagedObject { @NSManaged public private(set) var category: String? // many-to-one relationship - @NSManaged public private(set) var status: Status? + @NSManaged public private(set) var status: StatusLegacy? } public extension Emoji { diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 83f94fd22..6b7fe9893 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -62,19 +62,19 @@ final public class MastodonUser: NSManagedObject { @NSManaged public private(set) var updatedAt: Date // one-to-one relationship - @NSManaged public private(set) var pinnedStatus: Status? + @NSManaged public private(set) var pinnedStatus: StatusLegacy? @NSManaged public private(set) var mastodonAuthentication: MastodonAuthenticationLegacy? // one-to-many relationship - @NSManaged public private(set) var statuses: Set + @NSManaged public private(set) var statuses: Set @NSManaged public private(set) var notifications: Set @NSManaged public private(set) var searchHistories: Set // many-to-many relationship - @NSManaged public private(set) var favourite: Set - @NSManaged public private(set) var reblogged: Set - @NSManaged public private(set) var muted: Set - @NSManaged public private(set) var bookmarked: Set + @NSManaged public private(set) var favourite: Set + @NSManaged public private(set) var reblogged: Set + @NSManaged public private(set) var muted: Set + @NSManaged public private(set) var bookmarked: Set @NSManaged public private(set) var votePollOptions: Set @NSManaged public private(set) var votePolls: Set // relationships diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift index 7eec86842..f53263d23 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Notification.swift @@ -8,6 +8,7 @@ import Foundation import CoreData + public final class Notification: NSManagedObject { public typealias ID = String @@ -29,64 +30,64 @@ public final class Notification: NSManagedObject { // sourcery: autoGenerateRelationship @NSManaged public private(set) var account: MastodonUser // sourcery: autoGenerateRelationship - @NSManaged public private(set) var status: Status? + @NSManaged public private(set) var status: StatusLegacy? // 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 { +// // 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 { } @@ -220,11 +221,11 @@ extension Notification: AutoGenerateRelationship { // DO NOT EDIT public struct Relationship { public let account: MastodonUser - public let status: Status? + public let status: StatusLegacy? public init( account: MastodonUser, - status: Status? + status: StatusLegacy? ) { self.account = account self.status = status @@ -249,16 +250,6 @@ extension Notification: AutoUpdatableObject { 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 } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Poll.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Poll.swift index 6be59ea57..70e1e9323 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Poll.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Poll.swift @@ -38,7 +38,7 @@ public final class Poll: NSManagedObject { @NSManaged public private(set) var isVoting: Bool // one-to-one relationship - @NSManaged public private(set) var status: Status? + @NSManaged public private(set) var status: StatusLegacy? // one-to-many relationship @NSManaged public private(set) var options: Set diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/SearchHistory.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/SearchHistory.swift index c3c6d28c3..3ad856fbf 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/SearchHistory.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/SearchHistory.swift @@ -28,7 +28,7 @@ public final class SearchHistory: NSManagedObject { // sourcery: autoGenerateRelationship @NSManaged public private(set) var hashtag: Tag? // sourcery: autoGenerateRelationship - @NSManaged public private(set) var status: Status? + @NSManaged public private(set) var status: StatusLegacy? } @@ -122,12 +122,12 @@ extension SearchHistory: AutoGenerateRelationship { public struct Relationship { public let account: MastodonUser? public let hashtag: Tag? - public let status: Status? + public let status: StatusLegacy? public init( account: MastodonUser?, hashtag: Tag?, - status: Status? + status: StatusLegacy? ) { self.account = account self.hashtag = hashtag diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift index 1bdd9410a..552687e14 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Mastodon/Status.swift @@ -1,5 +1,5 @@ // -// Status.swift +// StatusLegacy.swift // CoreDataStack // // Created by MainasuK Cirno on 2021/1/27. @@ -8,7 +8,38 @@ import CoreData import Foundation -public final class Status: NSManagedObject { +public enum MastodonVisibilityLegacy: RawRepresentable { + case `public` + case unlisted + case `private` + case direct + + case _other(String) + + public init?(rawValue: String) { + switch rawValue { + case "public": self = .public + case "unlisted": self = .unlisted + case "private": self = .private + case "direct": self = .direct + default: self = ._other(rawValue) + } + } + + public var rawValue: String { + switch self { + case .public: return "public" + case .unlisted: return "unlisted" + case .private: return "private" + case .direct: return "direct" + case ._other(let value): return value + } + } +} + + +@objc(Status) +public final class StatusLegacy: NSManagedObject { public typealias ID = String // sourcery: autoGenerateProperty @@ -31,10 +62,10 @@ public final class Status: NSManagedObject { @NSManaged public private(set) var visibilityRaw: String // sourcery: autoUpdatableObject, autoGenerateProperty - public var visibility: MastodonVisibility { + public var visibility: MastodonVisibilityLegacy { get { let rawValue = visibilityRaw - return MastodonVisibility(rawValue: rawValue) ?? ._other(rawValue) + return MastodonVisibilityLegacy(rawValue: rawValue) ?? ._other(rawValue) } set { visibilityRaw = newValue.rawValue @@ -62,7 +93,7 @@ public final class Status: NSManagedObject { // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var url: String? // sourcery: autoUpdatableObject, autoGenerateProperty - @NSManaged public private(set) var inReplyToID: Status.ID? + @NSManaged public private(set) var inReplyToID: StatusLegacy.ID? // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var inReplyToAccountID: MastodonUser.ID? @@ -75,9 +106,9 @@ public final class Status: NSManagedObject { // sourcery: autoGenerateRelationship @NSManaged public private(set) var author: MastodonUser // sourcery: autoGenerateRelationship - @NSManaged public private(set) var reblog: Status? + @NSManaged public private(set) var reblog: StatusLegacy? // sourcery: autoUpdatableObject - @NSManaged public private(set) var replyTo: Status? + @NSManaged public private(set) var replyTo: StatusLegacy? // many-to-many relationship @NSManaged public private(set) var favouritedBy: Set @@ -95,8 +126,8 @@ public final class Status: NSManagedObject { // one-to-many relationship @NSManaged public private(set) var feeds: Set - @NSManaged public private(set) var reblogFrom: Set - @NSManaged public private(set) var replyFrom: Set + @NSManaged public private(set) var reblogFrom: Set + @NSManaged public private(set) var replyFrom: Set @NSManaged public private(set) var notifications: Set @NSManaged public private(set) var searchHistories: Set @@ -108,11 +139,11 @@ public final class Status: NSManagedObject { @NSManaged public private(set) var revealedAt: Date? } -extension Status { +extension StatusLegacy { // sourcery: autoUpdatableObject, autoGenerateProperty @objc public var attachments: [MastodonAttachment] { get { - let keyPath = #keyPath(Status.attachments) + let keyPath = #keyPath(StatusLegacy.attachments) willAccessValue(forKey: keyPath) let _data = primitiveValue(forKey: keyPath) as? Data didAccessValue(forKey: keyPath) @@ -126,7 +157,7 @@ extension Status { } } set { - let keyPath = #keyPath(Status.attachments) + let keyPath = #keyPath(StatusLegacy.attachments) let data = try? JSONEncoder().encode(newValue) willChangeValue(forKey: keyPath) setPrimitiveValue(data, forKey: keyPath) @@ -137,7 +168,7 @@ extension Status { // sourcery: autoUpdatableObject, autoGenerateProperty @objc public var emojis: [MastodonEmoji] { get { - let keyPath = #keyPath(Status.emojis) + let keyPath = #keyPath(StatusLegacy.emojis) willAccessValue(forKey: keyPath) let _data = primitiveValue(forKey: keyPath) as? Data didAccessValue(forKey: keyPath) @@ -151,7 +182,7 @@ extension Status { } } set { - let keyPath = #keyPath(Status.emojis) + let keyPath = #keyPath(StatusLegacy.emojis) let data = try? JSONEncoder().encode(newValue) willChangeValue(forKey: keyPath) setPrimitiveValue(data, forKey: keyPath) @@ -162,7 +193,7 @@ extension Status { // sourcery: autoUpdatableObject, autoGenerateProperty @objc public var mentions: [MastodonMention] { get { - let keyPath = #keyPath(Status.mentions) + let keyPath = #keyPath(StatusLegacy.mentions) willAccessValue(forKey: keyPath) let _data = primitiveValue(forKey: keyPath) as? Data didAccessValue(forKey: keyPath) @@ -176,7 +207,7 @@ extension Status { } } set { - let keyPath = #keyPath(Status.mentions) + let keyPath = #keyPath(StatusLegacy.mentions) let data = try? JSONEncoder().encode(newValue) willChangeValue(forKey: keyPath) setPrimitiveValue(data, forKey: keyPath) @@ -185,17 +216,17 @@ extension Status { } } -extension Status: FeedIndexable { } +extension StatusLegacy: FeedIndexable { } -extension Status { +extension StatusLegacy { @discardableResult public static func insert( into context: NSManagedObjectContext, property: Property, relationship: Relationship - ) -> Status { - let object: Status = context.insertObject() + ) -> StatusLegacy { + let object: StatusLegacy = context.insertObject() object.configure(property: property) object.configure(relationship: relationship) @@ -205,20 +236,20 @@ extension Status { } -extension Status: Managed { +extension StatusLegacy: Managed { public static var defaultSortDescriptors: [NSSortDescriptor] { - return [NSSortDescriptor(keyPath: \Status.createdAt, ascending: false)] + return [NSSortDescriptor(keyPath: \StatusLegacy.createdAt, ascending: false)] } } -extension Status { +extension StatusLegacy { static func predicate(domain: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Status.domain), domain) + return NSPredicate(format: "%K == %@", #keyPath(StatusLegacy.domain), domain) } static func predicate(id: String) -> NSPredicate { - return NSPredicate(format: "%K == %@", #keyPath(Status.id), id) + return NSPredicate(format: "%K == %@", #keyPath(StatusLegacy.id), id) } public static func predicate(domain: String, id: String) -> NSPredicate { @@ -229,7 +260,7 @@ extension Status { } static func predicate(ids: [String]) -> NSPredicate { - return NSPredicate(format: "%K IN %@", #keyPath(Status.id), ids) + return NSPredicate(format: "%K IN %@", #keyPath(StatusLegacy.id), ids) } public static func predicate(domain: String, ids: [String]) -> NSPredicate { @@ -240,18 +271,18 @@ extension Status { } public static func notDeleted() -> NSPredicate { - return NSPredicate(format: "%K == nil", #keyPath(Status.deletedAt)) + return NSPredicate(format: "%K == nil", #keyPath(StatusLegacy.deletedAt)) } public static func deleted() -> NSPredicate { - return NSPredicate(format: "%K != nil", #keyPath(Status.deletedAt)) + return NSPredicate(format: "%K != nil", #keyPath(StatusLegacy.deletedAt)) } } // MARK: - AutoGenerateProperty -extension Status: AutoGenerateProperty { - // sourcery:inline:Status.AutoGenerateProperty +extension StatusLegacy: AutoGenerateProperty { + // sourcery:inline:StatusLegacy.AutoGenerateProperty // Generated using Sourcery // DO NOT EDIT @@ -263,14 +294,14 @@ extension Status: AutoGenerateProperty { public let createdAt: Date public let editedAt: Date? public let content: String - public let visibility: MastodonVisibility + public let visibility: MastodonVisibilityLegacy public let sensitive: Bool public let spoilerText: String? public let reblogsCount: Int64 public let favouritesCount: Int64 public let repliesCount: Int64 public let url: String? - public let inReplyToID: Status.ID? + public let inReplyToID: StatusLegacy.ID? public let inReplyToAccountID: MastodonUser.ID? public let language: String? public let text: String? @@ -288,14 +319,14 @@ extension Status: AutoGenerateProperty { createdAt: Date, editedAt: Date?, content: String, - visibility: MastodonVisibility, + visibility: MastodonVisibilityLegacy, sensitive: Bool, spoilerText: String?, reblogsCount: Int64, favouritesCount: Int64, repliesCount: Int64, url: String?, - inReplyToID: Status.ID?, + inReplyToID: StatusLegacy.ID?, inReplyToAccountID: MastodonUser.ID?, language: String?, text: String?, @@ -382,22 +413,22 @@ extension Status: AutoGenerateProperty { } // MARK: - AutoGenerateRelationship -extension Status: AutoGenerateRelationship { - // sourcery:inline:Status.AutoGenerateRelationship +extension StatusLegacy: AutoGenerateRelationship { + // sourcery:inline:StatusLegacy.AutoGenerateRelationship // Generated using Sourcery // DO NOT EDIT public struct Relationship { public let application: Application? public let author: MastodonUser - public let reblog: Status? + public let reblog: StatusLegacy? public let poll: Poll? public let card: Card? public init( application: Application?, author: MastodonUser, - reblog: Status?, + reblog: StatusLegacy?, poll: Poll?, card: Card? ) { @@ -420,8 +451,8 @@ extension Status: AutoGenerateRelationship { } // MARK: - AutoUpdatableObject -extension Status: AutoUpdatableObject { - // sourcery:inline:Status.AutoUpdatableObject +extension StatusLegacy: AutoUpdatableObject { + // sourcery:inline:StatusLegacy.AutoUpdatableObject // Generated using Sourcery // DO NOT EDIT @@ -440,7 +471,7 @@ extension Status: AutoUpdatableObject { self.content = content } } - public func update(visibility: MastodonVisibility) { + public func update(visibility: MastodonVisibilityLegacy) { if self.visibility != visibility { self.visibility = visibility } @@ -480,7 +511,7 @@ extension Status: AutoUpdatableObject { self.url = url } } - public func update(inReplyToID: Status.ID?) { + public func update(inReplyToID: StatusLegacy.ID?) { if self.inReplyToID != inReplyToID { self.inReplyToID = inReplyToID } @@ -500,7 +531,7 @@ extension Status: AutoUpdatableObject { self.text = text } } - public func update(replyTo: Status?) { + public func update(replyTo: StatusLegacy?) { if self.replyTo != replyTo { self.replyTo = replyTo } @@ -540,11 +571,11 @@ extension Status: AutoUpdatableObject { public func update(liked: Bool, by mastodonUser: MastodonUser) { if liked { if !self.favouritedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).add(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.favouritedBy)).add(mastodonUser) } } else { if self.favouritedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.favouritedBy)).remove(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.favouritedBy)).remove(mastodonUser) } } } @@ -552,11 +583,11 @@ extension Status: AutoUpdatableObject { public func update(reblogged: Bool, by mastodonUser: MastodonUser) { if reblogged { if !self.rebloggedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).add(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.rebloggedBy)).add(mastodonUser) } } else { if self.rebloggedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.rebloggedBy)).remove(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.rebloggedBy)).remove(mastodonUser) } } } @@ -564,11 +595,11 @@ extension Status: AutoUpdatableObject { public func update(muted: Bool, by mastodonUser: MastodonUser) { if muted { if !self.mutedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).add(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.mutedBy)).add(mastodonUser) } } else { if self.mutedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.mutedBy)).remove(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.mutedBy)).remove(mastodonUser) } } } @@ -576,11 +607,11 @@ extension Status: AutoUpdatableObject { public func update(bookmarked: Bool, by mastodonUser: MastodonUser) { if bookmarked { if !self.bookmarkedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).add(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.bookmarkedBy)).add(mastodonUser) } } else { if self.bookmarkedBy.contains(mastodonUser) { - self.mutableSetValue(forKey: #keyPath(Status.bookmarkedBy)).remove(mastodonUser) + self.mutableSetValue(forKey: #keyPath(StatusLegacy.bookmarkedBy)).remove(mastodonUser) } } } @@ -590,8 +621,8 @@ extension Status: AutoUpdatableObject { } } -extension Status { +extension StatusLegacy { public func attach(feed: Feed) { - mutableSetValue(forKey: #keyPath(Status.feeds)).add(feed) + mutableSetValue(forKey: #keyPath(StatusLegacy.feeds)).add(feed) } } diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonFollowRequestState.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonFollowRequestState.swift index c348d6cee..606f3bba5 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonFollowRequestState.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonFollowRequestState.swift @@ -7,22 +7,3 @@ import SwiftUI -public final class MastodonFollowRequestState: NSObject, Codable { - public let state: State - - public init( - state: State - ) { - self.state = state - } -} - -extension MastodonFollowRequestState { - public enum State: String, Codable { - case none - case isAccepting - case isAccept - case isRejecting - case isReject - } -} diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift index 455230d5e..25430ad6a 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift +++ b/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonNotificationType.swift @@ -7,7 +7,7 @@ import Foundation -public enum MastodonNotificationType: RawRepresentable, Equatable { +public enum MastodonNotificationTypeLegacy: RawRepresentable, Equatable { case follow case followRequest case mention diff --git a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift index 8db326dfe..6765838e4 100644 --- a/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift +++ b/MastodonSDK/Sources/MastodonCore/Authentication/MastodonAuthenticationBox.swift @@ -45,14 +45,33 @@ extension MastodonAuthenticationBox { userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken), inMemoryCache: .sharedCache(for: authentication.userID) // todo: make sure this is really unique ) + + fetchMeAccount() } + private func fetchMeAccount() { + Task { + do { + let account = try await Mastodon.API.Account.accountInfo( + session: .shared, + domain: domain, + userID: userID, + authorization: userAuthorization + ).singleOutput().value + + self.inMemoryCache.meAccount = account + } catch { + assertionFailure("Failed to fetchMeAccount") + } + } + } } public class MastodonAccountInMemoryCache { @Published public var followingUserIds: [String] = [] @Published public var blockedUserIds: [String] = [] @Published public var followRequestedUserIDs: [String] = [] + @Published public var meAccount: Mastodon.Entity.Account? static var sharedCaches = [String: MastodonAccountInMemoryCache]() diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonStatus.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonStatus.swift index b7d33712f..27e35c2ed 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonStatus.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonStatus.swift @@ -8,7 +8,7 @@ import Foundation import CoreDataStack -extension Status { +extension StatusLegacy { // mark content sensitive when status contains spoilerText public var isContentSensitive: Bool { diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status+Property.swift index 2d054c83b..dcd497791 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status+Property.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status+Property.swift @@ -10,35 +10,35 @@ import CoreGraphics import CoreDataStack import MastodonSDK -extension Status.Property { - init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) { - self.init( - identifier: entity.id + "@" + domain, - domain: domain, - id: entity.id, - uri: entity.uri, - createdAt: entity.createdAt, - editedAt: entity.editedAt, - content: entity.content ?? "", - visibility: entity.mastodonVisibility, - sensitive: entity.sensitive ?? false, - spoilerText: entity.spoilerText, - reblogsCount: Int64(entity.reblogsCount), - favouritesCount: Int64(entity.favouritesCount), - repliesCount: Int64(entity.repliesCount ?? 0), - url: entity.url, - inReplyToID: entity.inReplyToID, - inReplyToAccountID: entity.inReplyToAccountID, - language: entity.language, - text: entity.text, - updatedAt: networkDate, - deletedAt: nil, - attachments: entity.mastodonAttachments, - emojis: entity.mastodonEmojis, - mentions: entity.mastodonMentions - ) - } -} +//extension StatusLegacy.Property { +// init(entity: Mastodon.Entity.Status, domain: String, networkDate: Date) { +// self.init( +// identifier: entity.id + "@" + domain, +// domain: domain, +// id: entity.id, +// uri: entity.uri, +// createdAt: entity.createdAt, +// editedAt: entity.editedAt, +// content: entity.content ?? "", +// visibility: entity.mastodonVisibility, +// sensitive: entity.sensitive ?? false, +// spoilerText: entity.spoilerText, +// reblogsCount: Int64(entity.reblogsCount), +// favouritesCount: Int64(entity.favouritesCount), +// repliesCount: Int64(entity.repliesCount ?? 0), +// url: entity.url, +// inReplyToID: entity.inReplyToID, +// inReplyToAccountID: entity.inReplyToAccountID, +// language: entity.language, +// text: entity.text, +// updatedAt: networkDate, +// deletedAt: nil, +// attachments: entity.mastodonAttachments, +// emojis: entity.mastodonEmojis, +// mentions: entity.mastodonMentions +// ) +// } +//} extension Mastodon.Entity.Status { public var mastodonVisibility: MastodonVisibility { diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift index 797abac7f..4bf6e87f6 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/Status.swift @@ -9,7 +9,7 @@ import CoreDataStack import Foundation import MastodonSDK -extension Status { +extension StatusLegacy { public enum SensitiveType { case none case all @@ -45,7 +45,7 @@ extension Status { // } //} -extension Status { +extension StatusLegacy { public var statusURL: URL { if let urlString = self.url, let url = URL(string: urlString) @@ -70,8 +70,8 @@ extension Status { // } //} -extension Status { - public var asRecord: ManagedObjectRecord { +extension StatusLegacy { + public var asRecord: ManagedObjectRecord { return .init(objectID: self.objectID) } } diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift index 682c83815..109b5e4e4 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/FeedFetchedResultsController.swift @@ -1,95 +1,95 @@ +//// +//// FeedFetchedResultsController.swift +//// FeedFetchedResultsController +//// +//// Created by Cirno MainasuK on 2021-8-19. +//// Copyright © 2021 Twidere. All rights reserved. +//// // -// FeedFetchedResultsController.swift -// FeedFetchedResultsController +//import Foundation +//import UIKit +//import Combine +//import CoreData +//import CoreDataStack +//import MastodonSDK // -// Created by Cirno MainasuK on 2021-8-19. -// Copyright © 2021 Twidere. All rights reserved. +//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] = [] +// +// 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) +// } +//} +// +//// MARK: - NSFetchedResultsControllerDelegate +//extension FeedFetchedResultsController: NSFetchedResultsControllerDelegate { +// public func controller( +// _ controller: NSFetchedResultsController, +// didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference +// ) { +// let snapshot = snapshot as NSDiffableDataSourceSnapshot +// self._objectIDs.send(snapshot.itemIdentifiers) +// } +//} // - -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] = [] - - 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) - } -} - -// 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..73367b494 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/StatusFetchedResultsController.swift @@ -1,103 +1,103 @@ +//// +//// StatusFetchedResultsController.swift +//// Mastodon +//// +//// Created by MainasuK Cirno on 2021-3-30. +//// // -// StatusFetchedResultsController.swift -// Mastodon +//import UIKit +//import Combine +//import CoreData +//import CoreDataStack +//import MastodonSDK // -// Created by MainasuK Cirno on 2021-3-30. +//public final class StatusFetchedResultsController: NSObject { // - -import UIKit -import Combine -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] = [] - - // 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) - } - -} - -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 - } - -} - -// 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 - } -} +// var disposeBag = Set() +// +//// let fetchedResultsController: NSFetchedResultsController +// +// // input +// @Published public var domain: String? = nil +// @Published public var statusIDs: [Mastodon.Entity.Status.ID] = [] +// +// // output +//// let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) +// @Published public private(set) var records: [Mastodon.Entity.Status] = [] +// +// 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) +// } +// +//} +// +//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 +// } +// +//} +// +////// 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 +//// } +////} diff --git a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift index 452fa2914..d21f10959 100644 --- a/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift +++ b/MastodonSDK/Sources/MastodonCore/FetchedResultsController/UserFetchedResultsController.swift @@ -1,111 +1,111 @@ +//// +//// UserFetchedResultsController.swift +//// Mastodon +//// +//// Created by MainasuK Cirno on 2021-7-7. +//// // -// UserFetchedResultsController.swift -// Mastodon +//import UIKit +//import Combine +//import CoreData +//import CoreDataStack +//import MastodonSDK // -// Created by MainasuK Cirno on 2021-7-7. +//public final class UserFetchedResultsController: NSObject { // - -import UIKit -import Combine -import CoreData -import CoreDataStack -import MastodonSDK - -public final class UserFetchedResultsController: NSObject { - - var disposeBag = Set() - - let fetchedResultsController: NSFetchedResultsController - - // input - @Published public var domain: String? = nil - @Published public var userIDs: [Mastodon.Entity.Account.ID] = [] - @Published public var additionalPredicate: NSPredicate? - - // output - let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) - @Published public private(set) var records: [ManagedObjectRecord] = [] - - public init( - managedObjectContext: NSManagedObjectContext, - domain: String?, - additionalPredicate: NSPredicate? - ) { - self.domain = domain ?? "" - self.fetchedResultsController = { - let fetchRequest = MastodonUser.sortedFetchRequest - fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: []) - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - return controller - }() - self.additionalPredicate = additionalPredicate - 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.CombineLatest3( - self.$domain.removeDuplicates(), - self.$userIDs.removeDuplicates(), - self.$additionalPredicate.removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] domain, ids, additionalPredicate in - guard let self = self else { return } - var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)] - if let additionalPredicate = additionalPredicate { - predicates.append(additionalPredicate) - } - self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) - do { - try self.fetchedResultsController.performFetch() - } catch { - assertionFailure(error.localizedDescription) - } - } - .store(in: &disposeBag) - } - -} - -extension UserFetchedResultsController { - - public func append(userIDs: [Mastodon.Entity.Account.ID]) { - var result = self.userIDs - for userID in userIDs where !result.contains(userID) { - result.append(userID) - } - self.userIDs = result - } - -} - -// MARK: - NSFetchedResultsControllerDelegate -extension UserFetchedResultsController: NSFetchedResultsControllerDelegate { - public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { - - let indexes = userIDs - 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 - } -} +// var disposeBag = Set() +// +// let fetchedResultsController: NSFetchedResultsController +// +// // input +// @Published public var domain: String? = nil +// @Published public var userIDs: [Mastodon.Entity.Account.ID] = [] +// @Published public var additionalPredicate: NSPredicate? +// +// // output +// let _objectIDs = CurrentValueSubject<[NSManagedObjectID], Never>([]) +// @Published public private(set) var records: [ManagedObjectRecord] = [] +// +// public init( +// managedObjectContext: NSManagedObjectContext, +// domain: String?, +// additionalPredicate: NSPredicate? +// ) { +// self.domain = domain ?? "" +// self.fetchedResultsController = { +// let fetchRequest = MastodonUser.sortedFetchRequest +// fetchRequest.predicate = MastodonUser.predicate(domain: domain ?? "", ids: []) +// fetchRequest.returnsObjectsAsFaults = false +// fetchRequest.fetchBatchSize = 20 +// let controller = NSFetchedResultsController( +// fetchRequest: fetchRequest, +// managedObjectContext: managedObjectContext, +// sectionNameKeyPath: nil, +// cacheName: nil +// ) +// +// return controller +// }() +// self.additionalPredicate = additionalPredicate +// 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.CombineLatest3( +// self.$domain.removeDuplicates(), +// self.$userIDs.removeDuplicates(), +// self.$additionalPredicate.removeDuplicates() +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] domain, ids, additionalPredicate in +// guard let self = self else { return } +// var predicates = [MastodonUser.predicate(domain: domain ?? "", ids: ids)] +// if let additionalPredicate = additionalPredicate { +// predicates.append(additionalPredicate) +// } +// self.fetchedResultsController.fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) +// do { +// try self.fetchedResultsController.performFetch() +// } catch { +// assertionFailure(error.localizedDescription) +// } +// } +// .store(in: &disposeBag) +// } +// +//} +// +//extension UserFetchedResultsController { +// +// public func append(userIDs: [Mastodon.Entity.Account.ID]) { +// var result = self.userIDs +// for userID in userIDs where !result.contains(userID) { +// result.append(userID) +// } +// self.userIDs = result +// } +// +//} +// +//// MARK: - NSFetchedResultsControllerDelegate +//extension UserFetchedResultsController: NSFetchedResultsControllerDelegate { +// public func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { +// +// let indexes = userIDs +// 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 +// } +//} diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift index 65650dcdc..b15071c49 100644 --- a/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift @@ -10,16 +10,17 @@ import Combine import CoreData import MastodonMeta import CoreDataStack +import MastodonSDK /// Note: update Equatable when change case enum ComposeStatusItem { - case replyTo(record: ManagedObjectRecord) - case input(replyTo: ManagedObjectRecord?, attribute: ComposeStatusAttribute) + case replyTo(record: Mastodon.Entity.Status) + case input(replyTo: Mastodon.Entity.Status?, attribute: ComposeStatusAttribute) case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute) case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute) } -extension ComposeStatusItem: Hashable { } +extension ComposeStatusItem: Hashable {} extension ComposeStatusItem { final class ComposeStatusAttribute: Hashable { diff --git a/MastodonSDK/Sources/MastodonCore/Model/MastodonStatusEntity.swift b/MastodonSDK/Sources/MastodonCore/Model/MastodonStatusEntity.swift new file mode 100644 index 000000000..ae5006e7c --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/MastodonStatusEntity.swift @@ -0,0 +1,21 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation +import MastodonSDK + +public class MastodonStatusEntity: Hashable { + public static func == (lhs: MastodonStatusEntity, rhs: MastodonStatusEntity) -> Bool { + lhs.status.id == rhs.status.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(status.id) + } + + public let status: Mastodon.Entity.Status + public var isSensitiveToggled: Bool = false + + public init(status: Mastodon.Entity.Status) { + self.status = status + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift index c0283cbd7..900f091af 100644 --- a/MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift @@ -11,6 +11,6 @@ import CoreDataStack import MastodonSDK public enum PollItem: Hashable { - case option(record: ManagedObjectRecord) + case option(record: Mastodon.Entity.Poll.Option, poll: Mastodon.Entity.Poll) case history(option: Mastodon.Entity.StatusEdit.Poll.Option) } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift index cfe715503..c2c855ae6 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Notification.swift @@ -68,21 +68,22 @@ extension Persistence.Notification { ) 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 status: StatusLegacy? = nil +// { +// 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, @@ -159,14 +160,14 @@ extension Persistence.Notification { ) 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) - } +// if let status = object.status, let entity = context.entity.status { +// let property = StatusLegacy.Property( +// entity: entity, +// domain: context.domain, +// networkDate: context.networkDate +// ) +// status.update(property: property) +// } let accountProperty = MastodonUser.Property( entity: context.entity.account, @@ -175,14 +176,14 @@ extension Persistence.Notification { ) 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) - } +// if let author = object.status, let entity = context.entity.status { +// let property = StatusLegacy.Property( +// entity: entity, +// domain: context.domain, +// networkDate: context.networkDate +// ) +// author.update(property: property) +// } update(object: object, context: context) } diff --git a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift index c7548db82..7c28fea43 100644 --- a/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Persistence/Persistence+Status.swift @@ -1,289 +1,289 @@ +//// +//// Persistence+Status.swift +//// Persistence+Status +//// +//// Created by Cirno MainasuK on 2021-8-27. +//// Copyright © 2021 Twidere. All rights reserved. +//// // -// Persistence+Status.swift -// Persistence+Status +//import CoreData +//import CoreDataStack +//import Foundation +//import MastodonSDK // -// Created by Cirno MainasuK on 2021-8-27. -// Copyright © 2021 Twidere. All rights reserved. +//extension Persistence.Status { +// +// public struct PersistContext { +// public let domain: String +// public let entity: Mastodon.Entity.Status +// public let me: MastodonUser? +// public let statusCache: Persistence.PersistCache? +// public let userCache: Persistence.PersistCache? +// public let networkDate: Date // - -import CoreData -import CoreDataStack -import Foundation -import MastodonSDK - -extension Persistence.Status { - - public struct PersistContext { - public let domain: String - public let entity: Mastodon.Entity.Status - public let me: MastodonUser? - public let statusCache: Persistence.PersistCache? - public let userCache: Persistence.PersistCache? - public let networkDate: Date - - public init( - domain: String, - entity: Mastodon.Entity.Status, - me: MastodonUser?, - statusCache: Persistence.PersistCache?, - userCache: Persistence.PersistCache?, - networkDate: Date - ) { - self.domain = domain - self.entity = entity - self.me = me - self.statusCache = statusCache - self.userCache = userCache - self.networkDate = networkDate - } - } - - public struct PersistResult { - public let status: Status - public let isNewInsertion: Bool - public let isNewInsertionAuthor: Bool - - public init( - status: Status, - isNewInsertion: Bool, - isNewInsertionAuthor: Bool - ) { - self.status = status - self.isNewInsertion = isNewInsertion - self.isNewInsertionAuthor = isNewInsertionAuthor - } - } - - public static func createOrMerge( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> PersistResult { - - let reblog = context.entity.reblog.flatMap { entity -> Status in - let result = createOrMerge( - in: managedObjectContext, - context: PersistContext( - domain: context.domain, - entity: entity, - me: context.me, - statusCache: context.statusCache, - userCache: context.userCache, - networkDate: context.networkDate - ) - ) - return result.status - } - - if let oldStatus = fetch(in: managedObjectContext, context: context) { - merge(in: managedObjectContext, mastodonStatus: oldStatus, context: context) - return PersistResult( - status: oldStatus, - isNewInsertion: false, - isNewInsertionAuthor: false - ) - } else { - let poll: Poll? = { - guard let entity = context.entity.poll else { return nil } - let result = Persistence.Poll.createOrMerge( - in: managedObjectContext, - context: Persistence.Poll.PersistContext( - domain: context.domain, - entity: entity, - me: context.me, - networkDate: context.networkDate - ) - ) - return result.poll - }() - - 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( - application: application, - author: author, - reblog: reblog, - poll: poll, - card: card - ) - let status = create( - in: managedObjectContext, - context: context, - relationship: relationship - ) - - return PersistResult( - status: status, - isNewInsertion: true, - isNewInsertionAuthor: authorResult.isNewInsertion - ) - } - } - -} - -extension Persistence.Status { - - public static func fetch( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> Status? { - if let cache = context.statusCache { - return cache.dictionary[context.entity.id] - } else { - let request = Status.sortedFetchRequest - request.predicate = Status.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, - relationship: Status.Relationship - ) -> Status { - let property = Status.Property( - entity: context.entity, - domain: context.domain, - networkDate: context.networkDate - ) - let status = Status.insert( - into: managedObjectContext, - property: property, - relationship: relationship - ) - update(status: status, context: context) - return status - } - - public static func merge( - in managedObjectContext: NSManagedObjectContext, - mastodonStatus status: Status, - context: PersistContext - ) { - guard context.networkDate > status.updatedAt else { return } - let property = Status.Property( - entity: context.entity, - domain: context.domain, - networkDate: context.networkDate - ) - status.update(property: property) - if let poll = status.poll, let entity = context.entity.poll { - // update poll - Persistence.Poll.update( - in: managedObjectContext, - poll: poll, - context: Persistence.Poll.PersistContext( - domain: context.domain, - entity: entity, - me: context.me, - networkDate: context.networkDate - ) - ) - } else if let entity = context.entity.poll { - // add poll - let result = Persistence.Poll.createOrMerge( - in: managedObjectContext, - context: Persistence.Poll.PersistContext( - domain: context.domain, - entity: entity, - me: context.me, - networkDate: context.networkDate - ) - ) - - status.configure( - relationship: - Status.Relationship( - application: status.application, - author: status.author, - reblog: status.reblog, - poll: result.poll, - card: status.card - ) - ) - } else if status.poll != nil, context.entity.poll == nil { - // remove poll - status.configure( - relationship: - Status.Relationship( - application: status.application, - author: status.author, - reblog: status.reblog, - poll: nil, - card: status.card - ) - ) - } - - if status.card == nil, context.entity.card != nil { - let card = createCard(in: managedObjectContext, context: context) - let relationship = Card.Relationship(status: status) - card?.configure(relationship: relationship) - } - - update(status: status, context: context) - } - - private static func createCard( - in managedObjectContext: NSManagedObjectContext, - context: PersistContext - ) -> Card? { - guard let entity = context.entity.card else { return nil } - let result = Persistence.Card.create( - in: managedObjectContext, - context: Persistence.Card.PersistContext( - domain: context.domain, - entity: entity, - me: context.me - ) - ) - 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, - context: MastodonApplication.PersistContext - ) -> Application? { - guard let application = context.entity.application else { return nil } - - let persistedApplication = Application.insert(into: managedObjectContext, property: .init(name: application.name, website: application.website, vapidKey: application.vapidKey)) - - return persistedApplication - } - - enum MastodonApplication { - public struct PersistContext { - let entity: Mastodon.Entity.Status - } - } -} +// public init( +// domain: String, +// entity: Mastodon.Entity.Status, +// me: MastodonUser?, +// statusCache: Persistence.PersistCache?, +// userCache: Persistence.PersistCache?, +// networkDate: Date +// ) { +// self.domain = domain +// self.entity = entity +// self.me = me +// self.statusCache = statusCache +// self.userCache = userCache +// self.networkDate = networkDate +// } +// } +// +// public struct PersistResult { +// public let status: Status +// public let isNewInsertion: Bool +// public let isNewInsertionAuthor: Bool +// +// public init( +// status: Status, +// isNewInsertion: Bool, +// isNewInsertionAuthor: Bool +// ) { +// self.status = status +// self.isNewInsertion = isNewInsertion +// self.isNewInsertionAuthor = isNewInsertionAuthor +// } +// } +// +// public static func createOrMerge( +// in managedObjectContext: NSManagedObjectContext, +// context: PersistContext +// ) -> PersistResult { +// +// let reblog = context.entity.reblog.flatMap { entity -> Status in +// let result = createOrMerge( +// in: managedObjectContext, +// context: PersistContext( +// domain: context.domain, +// entity: entity, +// me: context.me, +// statusCache: context.statusCache, +// userCache: context.userCache, +// networkDate: context.networkDate +// ) +// ) +// return result.status +// } +// +// if let oldStatus = fetch(in: managedObjectContext, context: context) { +// merge(in: managedObjectContext, mastodonStatus: oldStatus, context: context) +// return PersistResult( +// status: oldStatus, +// isNewInsertion: false, +// isNewInsertionAuthor: false +// ) +// } else { +// let poll: Poll? = { +// guard let entity = context.entity.poll else { return nil } +// let result = Persistence.Poll.createOrMerge( +// in: managedObjectContext, +// context: Persistence.Poll.PersistContext( +// domain: context.domain, +// entity: entity, +// me: context.me, +// networkDate: context.networkDate +// ) +// ) +// return result.poll +// }() +// +// 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( +// application: application, +// author: author, +// reblog: reblog, +// poll: poll, +// card: card +// ) +// let status = create( +// in: managedObjectContext, +// context: context, +// relationship: relationship +// ) +// +// return PersistResult( +// status: status, +// isNewInsertion: true, +// isNewInsertionAuthor: authorResult.isNewInsertion +// ) +// } +// } +// +//} +// +//extension Persistence.Status { +// +// public static func fetch( +// in managedObjectContext: NSManagedObjectContext, +// context: PersistContext +// ) -> Status? { +// if let cache = context.statusCache { +// return cache.dictionary[context.entity.id] +// } else { +// let request = Status.sortedFetchRequest +// request.predicate = Status.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, +// relationship: Status.Relationship +// ) -> Status { +// let property = Status.Property( +// entity: context.entity, +// domain: context.domain, +// networkDate: context.networkDate +// ) +// let status = Status.insert( +// into: managedObjectContext, +// property: property, +// relationship: relationship +// ) +// update(status: status, context: context) +// return status +// } +// +// public static func merge( +// in managedObjectContext: NSManagedObjectContext, +// mastodonStatus status: Status, +// context: PersistContext +// ) { +// guard context.networkDate > status.updatedAt else { return } +// let property = Status.Property( +// entity: context.entity, +// domain: context.domain, +// networkDate: context.networkDate +// ) +// status.update(property: property) +// if let poll = status.poll, let entity = context.entity.poll { +// // update poll +// Persistence.Poll.update( +// in: managedObjectContext, +// poll: poll, +// context: Persistence.Poll.PersistContext( +// domain: context.domain, +// entity: entity, +// me: context.me, +// networkDate: context.networkDate +// ) +// ) +// } else if let entity = context.entity.poll { +// // add poll +// let result = Persistence.Poll.createOrMerge( +// in: managedObjectContext, +// context: Persistence.Poll.PersistContext( +// domain: context.domain, +// entity: entity, +// me: context.me, +// networkDate: context.networkDate +// ) +// ) +// +// status.configure( +// relationship: +// Status.Relationship( +// application: status.application, +// author: status.author, +// reblog: status.reblog, +// poll: result.poll, +// card: status.card +// ) +// ) +// } else if status.poll != nil, context.entity.poll == nil { +// // remove poll +// status.configure( +// relationship: +// Status.Relationship( +// application: status.application, +// author: status.author, +// reblog: status.reblog, +// poll: nil, +// card: status.card +// ) +// ) +// } +// +// if status.card == nil, context.entity.card != nil { +// let card = createCard(in: managedObjectContext, context: context) +// let relationship = Card.Relationship(status: status) +// card?.configure(relationship: relationship) +// } +// +// update(status: status, context: context) +// } +// +// private static func createCard( +// in managedObjectContext: NSManagedObjectContext, +// context: PersistContext +// ) -> Card? { +// guard let entity = context.entity.card else { return nil } +// let result = Persistence.Card.create( +// in: managedObjectContext, +// context: Persistence.Card.PersistContext( +// domain: context.domain, +// entity: entity, +// me: context.me +// ) +// ) +// 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, +// context: MastodonApplication.PersistContext +// ) -> Application? { +// guard let application = context.entity.application else { return nil } +// +// let persistedApplication = Application.insert(into: managedObjectContext, property: .init(name: application.name, website: application.website, vapidKey: application.vapidKey)) +// +// return persistedApplication +// } +// +// enum MastodonApplication { +// public struct PersistContext { +// let entity: Mastodon.Entity.Status +// } +// } +//} diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift index ad9561565..ab4497956 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift @@ -164,31 +164,14 @@ 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.Tag.createOrMerge( - in: managedObjectContext, - context: Persistence.Tag.PersistContext( - domain: domain, - entity: entity, - me: me, - networkDate: response.networkDate - ) - ) - } - } - return response } // end func } 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 @@ -199,21 +182,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+Bookmark.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift index 8c4e48417..b0fd5ca7a 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Bookmark.swift @@ -14,87 +14,94 @@ import CoreDataStack extension APIService { private struct MastodonBookmarkContext { - let statusID: Status.ID + let statusID: Mastodon.Entity.Status.ID let isBookmarked: Bool } public func bookmark( - record: ManagedObjectRecord, + record: Mastodon.Entity.Status, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext +// let managedObjectContext = backgroundManagedObjectContext // update bookmark state and retrieve bookmark context - let bookmarkContext: MastodonBookmarkContext = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication +// 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) - } +// guard +// let _status = record.object(in: managedObjectContext), +// let me = authentication.user(in: managedObjectContext) +// else { +// throw APIError.implicit(.badRequest) +// } - let status = _status.reblog ?? _status - let isBookmarked = status.bookmarkedBy.contains(me) - status.update(bookmarked: !isBookmarked, by: me) - let context = MastodonBookmarkContext( - statusID: status.id, - isBookmarked: isBookmarked - ) - return context - } + let status = record.reblog ?? record +// let isBookmarked = status.bookmarkedBy.contains(me) +// status.update(bookmarked: !isBookmarked, by: me) +// let context = MastodonBookmarkContext( +// statusID: status.id, +// isBookmarked: isBookmarked +// ) +// return context +// } - // request bookmark or undo bookmark - let result: Result, Error> - do { - let response = try await Mastodon.API.Bookmarks.bookmarks( - domain: authenticationBox.domain, - statusID: bookmarkContext.statusID, - session: session, - authorization: authenticationBox.userAuthorization, - bookmarkKind: bookmarkContext.isBookmarked ? .destroy : .create - ).singleOutput() - result = .success(response) - } catch { - result = .failure(error) - } +// // request bookmark or undo bookmark +// let result: Result, Error> +// do { +// let response = try await Mastodon.API.Bookmarks.bookmarks( +// domain: authenticationBox.domain, +// statusID: status.id, +// session: session, +// authorization: authenticationBox.userAuthorization, +// bookmarkKind: status.bookmarked == true ? .destroy : .create +// ).singleOutput() +// result = .success(response) +// } catch { +// result = .failure(error) +// } - // 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) - } - } + let response = try await Mastodon.API.Bookmarks.bookmarks( + domain: authenticationBox.domain, + statusID: status.id, + session: session, + authorization: authenticationBox.userAuthorization, + bookmarkKind: status.bookmarked == true ? .destroy : .create + ).singleOutput() - 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) +// } +// } + +// let response = try result.get() return response } - } extension APIService { @@ -112,33 +119,33 @@ extension APIService { 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 - } +// 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 ec44afa93..264dc5fe9 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -14,93 +14,101 @@ import CoreDataStack extension APIService { private struct MastodonFavoriteContext { - let statusID: Status.ID + let statusID: Mastodon.Entity.Status.ID let isFavorited: Bool let favoritedCount: Int64 } public func favorite( - record: ManagedObjectRecord, + record: Mastodon.Entity.Status, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext +// let managedObjectContext = backgroundManagedObjectContext // update like state and retrieve like context - let favoriteContext: MastodonFavoriteContext = 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 favoriteContext: MastodonFavoriteContext = 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 = _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 context = MastodonFavoriteContext( - statusID: status.id, - isFavorited: isFavorited, - favoritedCount: favoritedCount - ) - return context - } + let status = record.reblog ?? record +// 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 context = MastodonFavoriteContext( +// statusID: status.id, +// isFavorited: isFavorited, +// favoritedCount: favoritedCount +// ) +// return context +// } // request like or undo like - let result: Result, Error> - do { - let response = try await Mastodon.API.Favorites.favorites( - domain: authenticationBox.domain, - statusID: favoriteContext.statusID, - session: session, - authorization: authenticationBox.userAuthorization, - favoriteKind: favoriteContext.isFavorited ? .destroy : .create - ).singleOutput() - result = .success(response) - } catch { - result = .failure(error) - } +// let result: Result, Error> +// do { +// let response = try await Mastodon.API.Favorites.favorites( +// domain: authenticationBox.domain, +// statusID: status.id, +// session: session, +// authorization: authenticationBox.userAuthorization, +// favoriteKind: status.favourited == true ? .destroy : .create +// ).singleOutput() +// result = .success(response) +// } catch { +// 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 await Mastodon.API.Favorites.favorites( + domain: authenticationBox.domain, + statusID: status.id, + session: session, + authorization: authenticationBox.userAuthorization, + favoriteKind: status.favourited == true ? .destroy : .create + ).singleOutput() - let response = try result.get() +// // 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 } @@ -121,30 +129,30 @@ extension APIService { 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 - } +// 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 @@ -152,41 +160,41 @@ extension APIService { extension APIService { public func favoritedBy( - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, 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 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 response = try await Mastodon.API.Statuses.favoriteBy( session: session, domain: authenticationBox.domain, - statusID: statusID, + statusID: _status.id, 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 - } +// 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+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index e31dbedce..301417a99 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -146,21 +146,29 @@ extension APIService { } public func toggleShowReblogs( - for user: ManagedObjectRecord, + for user: Mastodon.Entity.Account, 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 managedObjectContext = backgroundManagedObjectContext +// guard let me = authenticationBox.inMemoryCache.meAccount +// else { throw APIError.implicit(.badRequest) } let result: Result, Error> - let oldShowReblogs = me.showingReblogsBy.contains(user) - let newShowReblogs = (oldShowReblogs == false) +// let oldShowReblogs = me.showingReblogsBy.contains(user) +// let newShowReblogs = (oldShowReblogs == false) do { + let relation = try await Mastodon.API.Account.relationships( + session: session, + domain: authenticationBox.domain, + query: .init(ids: [user.id]), + authorization: authenticationBox.userAuthorization + ).singleOutput().value.first + + let newShowReblogs = relation?.showingReblogs == false + let response = try await Mastodon.API.Account.follow( session: session, domain: authenticationBox.domain, @@ -174,24 +182,24 @@ extension APIService { 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) - } - } +// 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() } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift index c8d1bfb73..58d5ebeff 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HashtagTimeline.swift @@ -42,24 +42,24 @@ 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 { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - } +// 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+HomeTimeline.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift index 5317cd4b0..fd623f4cb 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+HomeTimeline.swift @@ -41,7 +41,7 @@ extension APIService { authorization: authorization ).singleOutput() - let managedObjectContext = self.backgroundManagedObjectContext +// 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 @@ -54,70 +54,70 @@ 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) - } - } - } - } +// 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/MastodonCore/Service/API/APIService+Mute.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift index 65f0eb811..b676a0914 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Mute.swift @@ -60,32 +60,32 @@ extension APIService { } public func toggleMute( - user: ManagedObjectRecord, + user: Mastodon.Entity.Account, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext - let muteContext: MastodonMuteContext = 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 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 - ) + guard + let me = authenticationBox.inMemoryCache.meAccount + else { + throw APIError.implicit(.badRequest) } + + let relation = try await Mastodon.API.Account.relationships( + session: session, + domain: authenticationBox.domain, + query: .init(ids: [user.id]), + authorization: authenticationBox.userAuthorization + ).singleOutput().value.first + + let isMuting = relation?.muting == true + + // toggle mute state + let muteContext = MastodonMuteContext( + sourceUserID: me.id, + targetUserID: user.id, + targetUsername: user.username, + isMuting: isMuting + ) let result: Result, Error> do { @@ -112,28 +112,6 @@ extension APIService { 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 } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index 296a43d2b..3fd871bba 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -6,12 +6,9 @@ // import Combine -import CoreData -import CoreDataStack import Foundation import MastodonSDK import OSLog -import class CoreDataStack.Notification extension APIService { @@ -19,14 +16,14 @@ extension APIService { case mentions case everything - public var includeTypes: [MastodonNotificationType]? { + public var includeTypes: [Mastodon.Entity.Notification.NotificationType]? { switch self { case .everything: return nil case .mentions: return [.mention, .status] } } - public var excludeTypes: [MastodonNotificationType]? { + public var excludeTypes: [Mastodon.Entity.Notification.NotificationType]? { switch self { case .everything: return nil case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll] @@ -86,73 +83,73 @@ 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) - } - } - } - } +// 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 } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift index ef486448d..a00ec6f78 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Poll.swift @@ -7,46 +7,23 @@ import Foundation import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { public func poll( - poll: ManagedObjectRecord, + poll: Mastodon.Entity.Poll, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { let authorization = authenticationBox.userAuthorization - let managedObjectContext = self.backgroundManagedObjectContext - let pollID: Poll.ID = try await managedObjectContext.perform { - guard let poll = poll.object(in: managedObjectContext) else { - throw APIError.implicit(.badRequest) - } - return poll.id - } - let response = try await Mastodon.API.Polls.poll( session: session, domain: authenticationBox.domain, - pollID: pollID, + pollID: poll.id, authorization: authorization ).singleOutput() - - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - _ = Persistence.Poll.createOrMerge( - in: managedObjectContext, - context: Persistence.Poll.PersistContext( - domain: authenticationBox.domain, - entity: response.value, - me: me, - networkDate: response.networkDate - ) - ) - } - + return response } @@ -55,41 +32,19 @@ extension APIService { extension APIService { public func vote( - poll: ManagedObjectRecord, + poll: Mastodon.Entity.Poll, choices: [Int], authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext - let _pollID: Poll.ID? = try await managedObjectContext.perform { - guard let poll = poll.object(in: managedObjectContext) else { return nil } - return poll.id - } - - guard let pollID = _pollID else { - throw APIError.implicit(.badRequest) - } let response = try await Mastodon.API.Polls.vote( session: session, domain: authenticationBox.domain, - pollID: pollID, + pollID: poll.id, query: Mastodon.API.Polls.VoteQuery(choices: choices), authorization: authenticationBox.userAuthorization ).singleOutput() - - try await managedObjectContext.performChanges { - let me = authenticationBox.authentication.user(in: managedObjectContext) - _ = Persistence.Poll.createOrMerge( - in: managedObjectContext, - context: Persistence.Poll.PersistContext( - domain: authenticationBox.domain, - entity: response.value, - me: me, - 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..6f64c1cab 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+PublicTimeline.swift @@ -27,23 +27,23 @@ 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 { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - } +// 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+Reblog.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift index d3d5e1c15..029b22cba 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Reblog.swift @@ -14,110 +14,106 @@ import CoreDataStack extension APIService { private struct MastodonReblogContext { - let statusID: Status.ID + let statusID: Mastodon.Entity.Status.ID let isReblogged: Bool let rebloggedCount: Int64 } public func reblog( - record: ManagedObjectRecord, + record: Mastodon.Entity.Status, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { - let managedObjectContext = backgroundManagedObjectContext +// let managedObjectContext = backgroundManagedObjectContext // update repost state and retrieve repost context - let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges { - let authentication = authenticationBox.authentication +// let _reblogContext: MastodonReblogContext? = try await managedObjectContext.performChanges { +// let authentication = authenticationBox.authentication - guard - let me = authentication.user(in: managedObjectContext), - let _status = record.object(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 reblogContext = MastodonReblogContext( - statusID: status.id, - isReblogged: isReblogged, - rebloggedCount: rebloggedCount - ) - return reblogContext - } - guard let reblogContext = _reblogContext else { - throw APIError.implicit(.badRequest) - } +// guard +// let me = authentication.user(in: managedObjectContext), +// let _status = record.object(in: managedObjectContext) +// else { return nil } +// + let status = record.reblog ?? record +// 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 reblogContext = MastodonReblogContext( +// statusID: status.id, +// isReblogged: isReblogged, +// rebloggedCount: rebloggedCount +// ) +// return reblogContext +// } +// guard let reblogContext = _reblogContext else { +// throw APIError.implicit(.badRequest) +// } // request repost or undo repost - let result: Result, Error> - do { - let response = try await Mastodon.API.Reblog.reblog( - session: session, - domain: authenticationBox.domain, - statusID: reblogContext.statusID, - reblogKind: reblogContext.isReblogged ? .undoReblog : .reblog(query: Mastodon.API.Reblog.ReblogQuery(visibility: .public)), - authorization: authenticationBox.userAuthorization - ).singleOutput() - result = .success(response) - } catch { - result = .failure(error) - } + let response = try await Mastodon.API.Reblog.reblog( + session: session, + domain: authenticationBox.domain, + statusID: status.id, + reblogKind: record.reblogged == true ? .undoReblog : .reblog(query: Mastodon.API.Reblog.ReblogQuery(visibility: .public)), + authorization: authenticationBox.userAuthorization + ).singleOutput() - // 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 + +// // 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 result } } extension APIService { public func rebloggedBy( - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, 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 - } + let _statusID: Mastodon.Entity.Status.ID? = { + let _status = status.reblog ?? status + return _status.id + }() + guard let statusID = _statusID else { throw APIError.implicit(.badRequest) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift index 2df898977..4d3391f7b 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Relationship.swift @@ -7,24 +7,21 @@ import UIKit import Combine -import CoreData -import CoreDataStack import MastodonSDK extension APIService { public func relationship( - records: [ManagedObjectRecord], + records: [Mastodon.Entity.Account], 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: [MastodonUser.ID] = [] + var ids: [Mastodon.Entity.Account.ID] = [] 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 record.id != authenticationBox.userID else { continue } + ids.append(record.id) } guard !ids.isEmpty else { return nil } return Mastodon.API.Account.RelationshipQuery(ids: ids) @@ -40,28 +37,6 @@ extension APIService { 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 } @@ -71,7 +46,7 @@ extension APIService { authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> { - let ids: [MastodonUser.ID] = accounts.compactMap { $0.id } + let ids: [Mastodon.Entity.Account.ID] = accounts.compactMap { $0.id } guard ids.isEmpty == false else { throw APIError.implicit(.badRequest) } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift index df607df93..5e0bed179 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Search.swift @@ -25,38 +25,38 @@ extension APIService { 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 - ) - ) - } - - // 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 { … } +// 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 +// ) +// ) +// } +// +// // 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+History.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+History.swift index 523f95617..68013c3e0 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+History.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+History.swift @@ -7,7 +7,7 @@ import CoreDataStack extension APIService { public func getStatusSource( - forStatusID statusID: Status.ID, + forStatusID statusID: Mastodon.Entity.Status.ID, authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content { let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization @@ -22,7 +22,7 @@ extension APIService { } public func getHistory( - forStatusID statusID: Status.ID, + forStatusID statusID: Mastodon.Entity.Status.ID, authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content<[Mastodon.Entity.StatusEdit]> { let domain = authenticationBox.domain let authorization = authenticationBox.userAuthorization @@ -37,7 +37,7 @@ extension APIService { } public func publishStatusEdit( - forStatusID statusID: Status.ID, + forStatusID statusID: Mastodon.Entity.Status.ID, editStatusQuery: Mastodon.API.Statuses.EditStatusQuery, authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content { let domain = authenticationBox.domain diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift index 0f4896949..1802ec62e 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status+Publish.swift @@ -30,23 +30,23 @@ extension APIService { 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 +// #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 b67b79349..9f36fb413 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Status.swift @@ -27,52 +27,52 @@ extension APIService { 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 - ) - ) - } +// 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 } public func deleteStatus( - status: ManagedObjectRecord, + status: Mastodon.Entity.Status, 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.reblog ?? _status - return Mastodon.API.Statuses.DeleteStatusQuery(id: status.id) - } - guard let query = _query else { - throw APIError.implicit(.badRequest) - } +// 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.reblog ?? _status +// return Mastodon.API.Statuses.DeleteStatusQuery(id: status.id) +// } +// guard let query = _query else { +// throw APIError.implicit(.badRequest) +// } let response = try await Mastodon.API.Statuses.deleteStatus( session: session, domain: authenticationBox.domain, - query: query, + query: Mastodon.API.Statuses.DeleteStatusQuery(id: status.id), authorization: authorization ).singleOutput() - try await managedObjectContext.performChanges { - guard let status = status.object(in: managedObjectContext) else { return } - managedObjectContext.delete(status) - } +// try await managedObjectContext.performChanges { +// guard let status = status.object(in: managedObjectContext) else { return } +// managedObjectContext.delete(status) +// } return response } diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift index 7006a7477..d99b530c0 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Thread.swift @@ -27,25 +27,25 @@ extension APIService { 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 - ) - ) - } - } +// 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..773013dfc 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Trend.swift @@ -38,23 +38,23 @@ extension APIService { 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 - } - +// 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..4810bafe0 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+UserTimeline.swift @@ -43,23 +43,23 @@ 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 { - _ = Persistence.Status.createOrMerge( - in: managedObjectContext, - context: Persistence.Status.PersistContext( - domain: domain, - entity: entity, - me: me, - statusCache: nil, - userCache: nil, - networkDate: response.networkDate - ) - ) - } - } +// 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/MastodonSDK/Entity/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift index eb2910bb9..5a308d35b 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Account.swift @@ -85,6 +85,19 @@ extension Mastodon.Entity { } extension Mastodon.Entity.Account { + public var acctWithDomain: String { + if !acct.contains("@") { + // Safe concat due to username cannot contains "@" + guard let domain = domain else { + assertionFailure("domain is missing") + return username + } + return username + "@" + domain + } else { + return acct + } + } + public func acctWithDomainIfMissing(_ localDomain: String) -> String { guard acct.contains("@") else { return "\(acct)@\(localDomain)" @@ -103,3 +116,33 @@ extension Mastodon.Entity.Account { return components.host } } + +extension Mastodon.Entity.Account: Hashable { + public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { + lhs.domain == rhs.domain && lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(domain) + hasher.combine(id) + } +} + +extension Mastodon.Entity.Account { + + public var profileURL: URL { + if let url = URL(string: url) { + return url + } else { + #warning("fix domain!") + return URL(string: "https://\(domain!)/@\(username)")! + } + } + + public var activityItems: [Any] { + var items: [Any] = [] + items.append(profileURL) + return items + } + +} 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..d9a09ead0 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Notification.swift @@ -20,7 +20,7 @@ extension Mastodon.Entity { public typealias ID = String public let id: ID - public let type: Type + public let type: NotificationType public let createdAt: Date public let account: Account @@ -88,3 +88,14 @@ extension Mastodon.Entity.Notification { } } } + +extension Mastodon.Entity.Notification: Hashable { + public static func == (lhs: Mastodon.Entity.Notification, rhs: Mastodon.Entity.Notification) -> Bool { + lhs.account == rhs.account && lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(account) + hasher.combine(id) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift index c616b8b8f..e3405773b 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Poll.swift @@ -32,6 +32,9 @@ extension Mastodon.Entity { public let ownVotes: [Int]? public let options: [Option] + // virtual + public var isVoting = false + enum CodingKeys: String, CodingKey { case id case expiresAt = "expires_at" @@ -42,6 +45,7 @@ extension Mastodon.Entity { case voted case ownVotes = "own_votes" case options + case isVoting } } } @@ -60,3 +64,17 @@ extension Mastodon.Entity.Poll { } } } + +extension Mastodon.Entity.Poll: Hashable { + +} + +extension Mastodon.Entity.Poll.Option: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(title) + } + + public static func == (lhs: Mastodon.Entity.Poll.Option, rhs: Mastodon.Entity.Poll.Option) -> Bool { + lhs.title == rhs.title + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift index 66b9667a2..a3d8b9018 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Status.swift @@ -31,6 +31,7 @@ extension Mastodon.Entity { public let visibility: Visibility? public let sensitive: Bool? + public var sensitiveToggled: Bool = false public let spoilerText: String? public let mediaAttachments: [Attachment]? public let application: Application? @@ -72,6 +73,8 @@ extension Mastodon.Entity { case visibility case sensitive + case sensitiveToggled + case spoilerText = "spoiler_text" case mediaAttachments = "media_attachments" case application @@ -133,3 +136,23 @@ extension Mastodon.Entity.Status { } } } + +extension Mastodon.Entity.Status: Equatable { + public static func == (lhs: Mastodon.Entity.Status, rhs: Mastodon.Entity.Status) -> Bool { + lhs.id == rhs.id + } +} + +extension Mastodon.Entity.Status: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Mastodon.Entity.Status { + public var activityItems: [Any] { + var items: [Any] = [] + items.append(self.url) + return items + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/FeedItem.swift b/MastodonSDK/Sources/MastodonSDK/FeedItem.swift new file mode 100644 index 000000000..31e657012 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/FeedItem.swift @@ -0,0 +1,21 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation + +public struct FeedItem: Hashable { + public static func == (lhs: FeedItem, rhs: FeedItem) -> Bool { + lhs.status == rhs.status && lhs.notification == rhs.notification + } + + public let status: Mastodon.Entity.Status? + public let notification: Mastodon.Entity.Notification? + public let hasMore: Bool + public let isLoadingMore: Bool + + public init(status: Mastodon.Entity.Status?, notification: Mastodon.Entity.Notification?, hasMore: Bool, isLoadingMore: Bool) { + self.status = status + self.notification = notification + self.hasMore = hasMore + self.isLoadingMore = isLoadingMore + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFollowRequestState.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFollowRequestState.swift new file mode 100644 index 000000000..fbcea490b --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFollowRequestState.swift @@ -0,0 +1,23 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation + +public final class MastodonFollowRequestState: NSObject, Codable { + public let state: State + + public init( + state: State + ) { + self.state = state + } +} + +extension MastodonFollowRequestState { + public enum State: String, Codable { + case none + case isAccepting + case isAccept + case isRejecting + case isReject + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonNotificationType.swift b/MastodonSDK/Sources/MastodonSDK/MastodonNotificationType.swift new file mode 100644 index 000000000..77568fa32 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/MastodonNotificationType.swift @@ -0,0 +1,41 @@ +// Copyright © 2023 Mastodon gGmbH. All rights reserved. + +import Foundation + +public enum MastodonNotificationType11: RawRepresentable, Equatable { + case follow + case followRequest + case mention + case reblog + case favourite // same to API + case poll + case status + + case _other(String) + + public init?(rawValue: String) { + switch rawValue { + case "follow": self = .follow + case "follow_request": self = .followRequest + case "mention": self = .mention + case "reblog": self = .reblog + case "favourite": self = .favourite + case "poll": self = .poll + case "status": self = .status + default: self = ._other(rawValue) + } + } + + public var rawValue: String { + switch self { + case .follow: return "follow" + case .followRequest: return "follow_request" + case .mention: return "mention" + case .reblog: return "reblog" + case .favourite: return "favourite" + case .poll: return "poll" + case .status: return "status" + case ._other(let value): return value + } + } +} diff --git a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonVisibility.swift b/MastodonSDK/Sources/MastodonSDK/MastodonVisibility.swift similarity index 85% rename from MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonVisibility.swift rename to MastodonSDK/Sources/MastodonSDK/MastodonVisibility.swift index 798db208a..9db830716 100644 --- a/MastodonSDK/Sources/CoreDataStack/Entity/Transient/MastodonVisibility.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonVisibility.swift @@ -1,10 +1,4 @@ -// -// MastodonVisibility.swift -// MastodonVisibility -// -// Created by Cirno MainasuK on 2021-8-27. -// Copyright © 2021 Twidere. All rights reserved. -// +// Copyright © 2023 Mastodon gGmbH. All rights reserved. import Foundation diff --git a/MastodonSDK/Sources/MastodonUI/Extension/MastodonVisibility+Image.swift b/MastodonSDK/Sources/MastodonUI/Extension/MastodonVisibility+Image.swift index 05dcd5e18..5aff0d689 100644 --- a/MastodonSDK/Sources/MastodonUI/Extension/MastodonVisibility+Image.swift +++ b/MastodonSDK/Sources/MastodonUI/Extension/MastodonVisibility+Image.swift @@ -1,7 +1,7 @@ // Copyright © 2023 Mastodon gGmbH. All rights reserved. import UIKit -import CoreDataStack +import MastodonSDK import MastodonAsset extension MastodonVisibility { diff --git a/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift b/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift index 5977cba12..8eac1097f 100644 --- a/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift +++ b/MastodonSDK/Sources/MastodonUI/Protocol/StatusCompatible.swift @@ -2,12 +2,26 @@ import Foundation import CoreDataStack +import MastodonSDK +import MastodonCore public protocol StatusCompatible { - var reblog: Status? { get } + var reblog: Mastodon.Entity.Status? { get } var attachments: [MastodonAttachment] { get } var isMediaSensitive: Bool { get } var isSensitiveToggled: Bool { get } } -extension Status: StatusCompatible {} +extension MastodonStatusEntity: StatusCompatible{ + public var attachments: [MastodonAttachment] { + status.mastodonAttachments + } + + public var isMediaSensitive: Bool { + status.sensitive == true + } + + public var reblog: Mastodon.Entity.Status? { + status.reblog + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift index 9f036643b..c69f212a4 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -60,10 +60,10 @@ 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) - } +// context.managedObjectContext.performAndWait { +// guard let replyTo = status.object(in: context.managedObjectContext) else { return } + cell.statusView.configure(status: status) +// } } } } diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 1e669d654..117a4edb5 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: Mastodon.Entity.Status, statusSource: Mastodon.Entity.StatusSource) } var disposeBag = Set() @@ -162,12 +162,12 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { return author.locked ? .private : .public }() // 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 - } + if case .reply(let status) = destination { +// context.managedObjectContext.performAndWait { +// guard let status = record.object(in: context.managedObjectContext) else { +// assertionFailure() +// return +// } let repliedStatusVisibility = status.visibility switch repliedStatusVisibility { case .public, .unlisted: @@ -179,9 +179,10 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { visibility = .direct case ._other: assertionFailure() + case .none: break } - } +// } } return visibility }() @@ -191,7 +192,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { ) if case let ComposeContext.editStatus(status, _) = composeContext { - if status.isContentSensitive { + if status.sensitive == true { isContentWarningActive = true contentWarning = status.spoilerText ?? "" } @@ -201,7 +202,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { if let pollExpiresAt = poll.expiresAt { pollExpireConfigurationOption = .init(closestDateToExpiry: pollExpiresAt) } - pollOptions = poll.options.sortedByIndex().map { + pollOptions = poll.options.map { let option = PollComposeItem.Option() option.text = $0.title return option @@ -218,19 +219,19 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // setup initial value let initialContentWithSpace = initialContent.isEmpty ? "" : initialContent + " " switch destination { - case .reply(let record): - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { - assertionFailure() - return - } + case .reply(let status): +// context.managedObjectContext.performAndWait { +// guard let status = record.object(in: context.managedObjectContext) else { +// assertionFailure() +// return +// } 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 @@ -249,7 +250,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { let preInsertedContent = initialComposeContent.isEmpty ? "" : initialComposeContent + " " self.initialContent = preInsertedContent + initialContentWithSpace self.content = preInsertedContent + initialContentWithSpace - } +// } case .topLevel: self.initialContent = initialContentWithSpace self.content = initialContentWithSpace @@ -288,22 +289,22 @@ 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 vis = status.visibility, let visibility = Mastodon.Entity.Status.Visibility(rawValue: vis.rawValue) { self.visibility = visibility } self.isVisibilityButtonEnabled = false - self.attachmentViewModels = status.attachments.compactMap { - guard let assetURL = $0.assetURL, let url = URL(string: assetURL) else { return nil } + self.attachmentViewModels = status.mediaAttachments?.compactMap { att -> AttachmentViewModel? in + guard let assetURL = att.url, let url = URL(string: assetURL) else { return nil } let attachmentViewModel = AttachmentViewModel( api: context.apiService, authContext: authContext, - input: .mastodonAssetUrl(url, $0.id), + input: .mastodonAssetUrl(url, att.id), sizeLimit: sizeLimit, delegate: self ) - attachmentViewModel.caption = $0.altDescription ?? "" + attachmentViewModel.caption = att.description ?? "" return attachmentViewModel - } + } ?? [] } bind() @@ -503,7 +504,7 @@ extension ComposeContentViewModel { extension ComposeContentViewModel { public enum Destination { case topLevel - case reply(parent: ManagedObjectRecord) + case reply(parent: Mastodon.Entity.Status) } public enum ScrollViewState { diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift index 893fb1a28..40f2ce781 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusEditPublisher.swift @@ -10,7 +10,7 @@ import Combine public final class MastodonEditStatusPublisher: NSObject, ProgressReporting { // Input - public let statusID: Status.ID + public let statusID: Mastodon.Entity.Status.ID public let author: ManagedObjectRecord // content warning @@ -40,7 +40,7 @@ public final class MastodonEditStatusPublisher: NSObject, ProgressReporting { public var reactor: StatusPublisherReactor? public init( - statusID: Status.ID, + statusID: Mastodon.Entity.Status.ID, author: ManagedObjectRecord, isContentWarningComposing: Bool, contentWarning: String, diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index dfa7d3ef7..8f9e07914 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: Mastodon.Entity.Status? // 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: Mastodon.Entity.Status?, 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 } //?.object(in: api.backgroundManagedObjectContext) 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..90bb6ab2b 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: Mastodon.Entity.Status) -> [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.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.sensitive == true ? status.sensitiveToggled : 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..3a0f52fab 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView+ViewModel.swift @@ -13,18 +13,15 @@ import MastodonAsset import MastodonLocalization import MastodonExtension import MastodonCore -import CoreData -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? - @Published public var type: MastodonNotificationType? + @Published public var type: Mastodon.Entity.Notification.NotificationType? @Published public var notificationIndicatorText: MetaContent? @Published public var authorAvatarImage: UIImage? @@ -165,7 +162,9 @@ extension NotificationView.ViewModel { ) .sink { avatarImage, type in var actions = [UIAccessibilityCustomAction]() - + + guard let type else { return } + // these notifications can be directly actioned to view the profile if type != .follow, type != .followRequest { actions.append( diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index 394904dc1..6281e8cbd 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -179,8 +179,6 @@ public final class NotificationView: UIView { public func prepareForReuse() { disposeBag.removeAll() - viewModel.objects.removeAll() - viewModel.authContext = nil viewModel.authorAvatarImageURL = nil avatarButton.avatarImageView.cancelTask() diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift index fcf83e75b..a66a0e244 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/ProfileCardView+Configuration.swift @@ -7,7 +7,6 @@ import Foundation import Combine -import CoreDataStack import Meta import MastodonCore import MastodonMeta @@ -15,76 +14,48 @@ import MastodonSDK extension ProfileCardView { - public func configure(user: MastodonUser) { + public func configure(user: Mastodon.Entity.Account) { // banner - user.publisher(for: \.header) - .map { URL(string: $0) } - .assign(to: \.authorBannerImageURL, on: viewModel) - .store(in: &disposeBag) + viewModel.authorBannerImageURL = URL(string: user.header) + // author avatar - Publishers.CombineLatest3( - user.publisher(for: \.avatar), - user.publisher(for: \.avatarStatic), - UserDefaults.shared.publisher(for: \.preferredStaticAvatar) - ) - .map { _ in user.avatarImageURL() } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) + viewModel.authorAvatarImageURL = user.avatarImageURL() + // name - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _, emojis in + viewModel.authorName = { do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: emojis.asDictionary) + let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.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) + }() + // username - user.publisher(for: \.acct) - .map { $0 as String? } - .assign(to: \.authorUsername, on: viewModel) - .store(in: &disposeBag) + viewModel.authorUsername = user.acct + // bio - Publishers.CombineLatest( - user.publisher(for: \.note), - user.publisher(for: \.emojis) - ) - .map { note, emojis in - guard let note = note else { return nil } + viewModel.bioContent = { + guard !user.note.isEmpty else { return nil } do { - let content = MastodonContent(content: note, emojis: emojis.asDictionary) + let content = MastodonContent(content: user.note, emojis: user.emojis?.asDictionary ?? [:]) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { assertionFailure(error.localizedDescription) return nil } - } - .assign(to: \.bioContent, on: viewModel) - .store(in: &disposeBag) + }() + // relationship viewModel.relationshipViewModel.user = user + // 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) + viewModel.statusesCount = user.statusesCount + viewModel.followingCount = user.followingCount + viewModel.followersCount = user.followersCount } } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 5fead2f3c..f4a658342 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,22 @@ 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: url, placeholderImage: icon(for: card.layout) ) { [weak self] image, _, _, _ in if image != nil { @@ -333,6 +336,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..00a731c69 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -19,38 +19,28 @@ extension StatusView { static let statusFilterWorkingQueue = DispatchQueue(label: "StatusFilterWorkingQueue") - public func configure(feed: Feed) { - switch feed.kind { - case .home: - guard let status = feed.status else { - assertionFailure() - return - } - configure(status: status) - case .notificationAll: - assertionFailure("TODO") - case .notificationMentions: - assertionFailure("TODO") - case .none: - break + public func configure(feed: FeedItem) { + guard let status = feed.status else { + assertionFailure() + return } - + configure(status: status) } } 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: Mastodon.Entity.Status, statusEdit: Mastodon.Entity.StatusEdit) { +// viewModel.objects.insert(status) +// if let reblog = status.reblog { +// viewModel.objects.insert(status) +// } configureHeader(status: status) - let author = (status.reblog ?? status).author + let author = (status.reblog ?? status).account configureAuthor(author: author) - let timestamp = (status.reblog ?? status).publisher(for: \.createdAt) - configureTimestamp(timestamp: timestamp.eraseToAnyPublisher()) +// let timestamp = (status.reblog ?? status).publisher(for: \.createdAt) + configureTimestamp(timestamp: (status.reblog ?? status).createdAt) configureApplicationName(status.application?.name) configureMedia(status: status) configurePollHistory(statusEdit: statusEdit) @@ -66,17 +56,12 @@ 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: Mastodon.Entity.Status) { configureHeader(status: status) - let author = (status.reblog ?? status).author + let author = (status.reblog ?? status).account configureAuthor(author: author) - let timestamp = (status.reblog ?? status).publisher(for: \.createdAt) - configureTimestamp(timestamp: timestamp.eraseToAnyPublisher()) + let timestamp = (status.reblog ?? status).createdAt + configureTimestamp(timestamp: timestamp) configureApplicationName(status.application?.name) configureContent(status: status) configureMedia(status: status) @@ -96,14 +81,13 @@ extension StatusView { } extension StatusView { - private func configureHeader(status: Status) { + private func configureHeader(status: Mastodon.Entity.Status) { 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.account.displayName + let emojis = status.account.emojis ?? [] + + viewModel.header = { + let text = L10n.Common.Controls.Status.userReblogged(status.account.displayNameWithFallback) let content = MastodonContent(content: text, emojis: emojis.asDictionary) do { let metaContent = try MastodonMetaContent.convert(document: content) @@ -112,10 +96,8 @@ 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 { @@ -139,20 +121,31 @@ extension StatusView { return header } - if let replyTo = status.replyTo { + if let inReplyToID = status.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 +171,7 @@ extension StatusView { } .store(in: &disposeBag) } // end if let - } // end else B2. +// } // end else B2. } // end else B. } else { @@ -186,90 +179,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 +236,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 +250,7 @@ extension StatusView { configure(status: originalStatus) } - func configureTranslated(status: Status) { + func configureTranslated(status: Mastodon.Entity.Status) { guard let translation = viewModel.translation, let translatedContent = translation.content else { viewModel.isCurrentlyTranslating = false @@ -303,7 +259,7 @@ extension StatusView { // content do { - let content = MastodonContent(content: translatedContent, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: translatedContent, emojis: status.emojis?.asDictionary ?? [:]) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent viewModel.isCurrentlyTranslating = false @@ -313,7 +269,7 @@ extension StatusView { } } - private func configureContent(statusEdit: Mastodon.Entity.StatusEdit, status: Status) { + private func configureContent(statusEdit: Mastodon.Entity.StatusEdit, status: Mastodon.Entity.Status) { statusEdit.spoilerText.map { viewModel.spoilerContent = PlaintextMetaContent(string: $0) } @@ -332,7 +288,7 @@ extension StatusView { } } - private func configureContent(status: Status) { + private func configureContent(status: Mastodon.Entity.Status) { guard viewModel.translation == nil else { return configureTranslated(status: status) } @@ -342,7 +298,7 @@ extension StatusView { // spoilerText if let spoilerText = status.spoilerText, !spoilerText.isEmpty { do { - let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: spoilerText, emojis: status.emojis?.asDictionary ?? [:]) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.spoilerContent = metaContent } catch { @@ -356,7 +312,7 @@ extension StatusView { viewModel.language = (status.reblog ?? status).language // content do { - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: status.content ?? "", emojis: status.emojis?.asDictionary ?? [:]) let metaContent = try MastodonMetaContent.convert(document: content) viewModel.content = metaContent viewModel.isCurrentlyTranslating = false @@ -365,21 +321,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.mastodonVisibility + // sensitive - viewModel.isContentSensitive = status.isContentSensitive - status.publisher(for: \.isSensitiveToggled) - .assign(to: \.isSensitiveToggled, on: viewModel) - .store(in: &disposeBag) + viewModel.isContentSensitive = status.sensitive == true + viewModel.isSensitiveToggled = status.sensitiveToggled + } - private func configureMedia(status: StatusCompatible) { + private func configureMedia(status: Mastodon.Entity.Status) { let status = status.reblog ?? status - viewModel.isMediaSensitive = status.isMediaSensitive + viewModel.isMediaSensitive = status.sensitive == true let configurations = MediaView.configuration(status: status) viewModel.mediaViewConfigurations = configurations @@ -405,146 +358,84 @@ extension StatusView { pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot) } - private func configurePoll(status: Status) { + private func configurePoll(status: Mastodon.Entity.Status) { let status = status.reblog ?? status - if let poll = status.poll { - viewModel.objects.insert(poll) - } +// if let poll = status.poll { +// viewModel.objects.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 - } - - 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 + viewModel.pollItems = { + guard let poll = status.poll else { + return [] } - .store(in: &disposeBag) + +// let options = poll.options.sorted(by: { $0. < $1.index }) +// let items: [PollItem] = options.map { .option(record: .init(objectID: $0.objectID)) } + return poll.options.map { .option(record: $0, poll: poll) } + }() // 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) + if let poll = status.poll { + viewModel.isVoteButtonEnabled = { +// guard let poll = status.poll else { return false } +// let options = poll.options +// return options.contains(where: { $0.isSelected }) + return poll.voted == false + }() + } + // isVotable if let poll = status.poll { - 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 - } - .assign(to: &viewModel.$isVotable) + viewModel.isVotable = poll.voted == false && !poll.expired } + // votesCount - status.poll?.publisher(for: \.votesCount) - .map { Int($0) } - .assign(to: \.voteCount, on: viewModel) - .store(in: &disposeBag) + viewModel.voteCount = status.poll?.votesCount ?? 0 + // voterCount - status.poll?.publisher(for: \.votersCount) - .map { Int($0) } - .assign(to: \.voterCount, on: viewModel) - .store(in: &disposeBag) + viewModel.voterCount = status.poll?.votersCount + // expireAt - status.poll?.publisher(for: \.expiresAt) - .assign(to: \.expireAt, on: viewModel) - .store(in: &disposeBag) + viewModel.expireAt = status.poll?.expiresAt + // expired - status.poll?.publisher(for: \.expired) - .assign(to: \.expired, on: viewModel) - .store(in: &disposeBag) + viewModel.expired = status.poll?.expired == true + // isVoting - status.poll?.publisher(for: \.isVoting) - .assign(to: \.isVoting, on: viewModel) - .store(in: &disposeBag) + viewModel.isVoting = status.poll?.isVoting == true } - private func configureCard(status: Status) { + private func configureCard(status: Mastodon.Entity.Status) { let status = status.reblog ?? status if viewModel.mediaViewConfigurations.isEmpty { - status.publisher(for: \.card) - .assign(to: \.card, on: viewModel) - .store(in: &disposeBag) + viewModel.card = status.card } else { viewModel.card = nil } } - private func configureToolbar(status: Status) { + private func configureToolbar(status: Mastodon.Entity.Status) { 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.repliesCount ?? 0 + + viewModel.reblogCount = status.reblogsCount + + viewModel.favoriteCount = status.favouritesCount + + viewModel.editedAt = status.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.reblogged == true + viewModel.isFavorite = status.favourited == true + viewModel.isBookmark = status.bookmarked == true } - private func configureFilter(status: Status) { + private func configureFilter(status: Mastodon.Entity.Status) { let status = status.reblog ?? status - let content = status.content.lowercased() + guard let content = status.content?.lowercased() else { return } Publishers.CombineLatest( viewModel.$activeFilters, diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 2ff5b6f85..cec7b4a2b 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -22,11 +22,11 @@ extension StatusView { public final class ViewModel: ObservableObject { var disposeBag = Set() var observations = Set() - public var objects = Set() +// public var objects = Set() public var context: AppContext? public var authContext: AuthContext? - public var originalStatus: Status? + public var originalStatus: Mastodon.Entity.Status? // Header @Published public var header: Header = .none @@ -77,7 +77,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..20f50b2ce 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -307,7 +307,6 @@ public final class StatusView: UIView { public func prepareForReuse() { disposeBag.removeAll() - viewModel.objects.removeAll() viewModel.prepareForReuse() authorView.avatarButton.avatarImageView.cancelTask() @@ -404,7 +403,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/Content/UserView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift index f35afb97b..e35d39ec5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView+ViewModel.swift @@ -5,7 +5,6 @@ // Created by MainasuK on 2022-1-19. // -import CoreDataStack import UIKit import Combine import MetaTextKit @@ -26,7 +25,7 @@ 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 user: Mastodon.Entity.Account? @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..07e38fc64 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/UserView.swift @@ -11,12 +11,11 @@ import MetaTextKit import MastodonAsset import MastodonLocalization import os -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) + func userView(_ view: UserView, didTapButtonWith state: UserView.ButtonState, for user: Mastodon.Entity.Account, me: Mastodon.Entity.Account?) } public final class UserView: UIView { diff --git a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift index d4767048a..ab9475534 100644 --- a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/ProfileCardTableViewCell+Configuration.swift @@ -6,14 +6,13 @@ // import UIKit -import CoreDataStack import MastodonSDK extension ProfileCardTableViewCell { public func configure( tableView: UITableView, - user: MastodonUser, + user: Mastodon.Entity.Account, profileCardTableViewCellDelegate: ProfileCardTableViewCellDelegate? ) { if profileCardView.frame == .zero { diff --git a/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift index 19f2bbdaa..dc9e44d84 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: FeedItem, 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 } diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift index 20f720d20..a88dbbc05 100644 --- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift @@ -9,7 +9,7 @@ import UIKit import Combine import MastodonAsset import MastodonLocalization -import CoreDataStack +import MastodonSDK public enum RelationshipAction: Int, CaseIterable { case showReblogs @@ -104,8 +104,8 @@ public final class RelationshipViewModel { public var meObserver: AnyCancellable? // input - @Published public var user: MastodonUser? - @Published public var me: MastodonUser? + @Published public var user: Mastodon.Entity.Account? + @Published public var me: Mastodon.Entity.Account? public let relationshipUpdatePublisher = CurrentValueSubject(Void()) // needs initial event // output @@ -129,140 +129,140 @@ public final class RelationshipViewModel { .receive(on: DispatchQueue.main) .sink { [weak self] user, me, _ in guard let self = self else { return } - self.update(user: user, me: me) +// self.update(user: user, me: me) - guard let user = user, let me = me else { - self.userObserver = nil - self.meObserver = nil - return - } +// 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() - } +// // 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 { +// +// 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 - } -} +//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 +// } +//}