diff --git a/DB/Sources/DB/Content/ContentDatabase.swift b/DB/Sources/DB/Content/ContentDatabase.swift index fd5b94e..100a085 100644 --- a/DB/Sources/DB/Content/ContentDatabase.swift +++ b/DB/Sources/DB/Content/ContentDatabase.swift @@ -544,7 +544,7 @@ public extension ContentDatabase { accountIds.firstIndex(of: $0.record.id) ?? 0 < accountIds.firstIndex(of: $1.record.id) ?? 0 } - .map { CollectionItem.account(.init(info: $0), .withoutNote) } + .map { CollectionItem.account(.init(info: $0), .withoutNote, nil) } // TODO: revisit if let limit = limit, accounts.count >= limit { accounts.append(.moreResults(.init(scope: .accounts))) @@ -568,7 +568,8 @@ public extension ContentDatabase { CollectionItem.status( .init(info: $0), .init(showContentToggled: $0.showContentToggled, - showAttachmentsToggled: $0.showAttachmentsToggled)) + showAttachmentsToggled: $0.showAttachmentsToggled), + $0.reblogRelationship ?? $0.relationship) } if let limit = limit, statuses.count >= limit { diff --git a/DB/Sources/DB/Content/ContextItemsInfo.swift b/DB/Sources/DB/Content/ContextItemsInfo.swift index 5ae0a9b..4ff747e 100644 --- a/DB/Sources/DB/Content/ContextItemsInfo.swift +++ b/DB/Sources/DB/Content/ContextItemsInfo.swift @@ -49,7 +49,8 @@ extension ContextItemsInfo { showAttachmentsToggled: statusInfo.showAttachmentsToggled, isContextParent: isContextParent, isReplyInContext: isReplyInContext, - hasReplyFollowing: hasReplyFollowing)) + hasReplyFollowing: hasReplyFollowing), + statusInfo.reblogRelationship ?? statusInfo.relationship) } } .map { CollectionSection(items: $0) } diff --git a/DB/Sources/DB/Content/StatusInfo.swift b/DB/Sources/DB/Content/StatusInfo.swift index 823818f..9ba7c85 100644 --- a/DB/Sources/DB/Content/StatusInfo.swift +++ b/DB/Sources/DB/Content/StatusInfo.swift @@ -2,12 +2,15 @@ import Foundation import GRDB +import Mastodon struct StatusInfo: Codable, Hashable, FetchableRecord { let record: StatusRecord let accountInfo: AccountInfo + let relationship: Relationship? let reblogAccountInfo: AccountInfo? let reblogRecord: StatusRecord? + let reblogRelationship: Relationship? let showContentToggle: StatusShowContentToggle? let reblogShowContentToggle: StatusShowContentToggle? let showAttachmentsToggle: StatusShowAttachmentsToggle? @@ -50,7 +53,9 @@ private extension StatusInfo { static func addingOptionalIncludes(_ request: T) -> T where T.RowDecoder == StatusRecord { request.including(optional: AccountInfo.addingIncludes(StatusRecord.reblogAccount) .forKey(CodingKeys.reblogAccountInfo)) + .including(optional: StatusRecord.relationship.forKey(CodingKeys.relationship)) .including(optional: StatusRecord.reblog.forKey(CodingKeys.reblogRecord)) + .including(optional: StatusRecord.reblogRelationship.forKey(CodingKeys.reblogRelationship)) .including(optional: StatusRecord.showContentToggle.forKey(CodingKeys.showContentToggle)) .including(optional: StatusRecord.reblogShowContentToggle.forKey(CodingKeys.reblogShowContentToggle)) .including(optional: StatusRecord.showAttachmentsToggle.forKey(CodingKeys.showAttachmentsToggle)) diff --git a/DB/Sources/DB/Content/StatusRecord.swift b/DB/Sources/DB/Content/StatusRecord.swift index 2e9109e..a235037 100644 --- a/DB/Sources/DB/Content/StatusRecord.swift +++ b/DB/Sources/DB/Content/StatusRecord.swift @@ -72,6 +72,9 @@ extension StatusRecord { extension StatusRecord { static let account = belongsTo(AccountRecord.self) + static let relationship = hasOne(Relationship.self, + through: Self.account, + using: AccountRecord.relationship) static let accountMoved = hasOne(AccountRecord.self, through: Self.account, using: AccountRecord.moved) @@ -82,6 +85,10 @@ extension StatusRecord { through: Self.reblogAccount, using: AccountRecord.moved) static let reblog = belongsTo(StatusRecord.self) + static let reblogRelationship = hasOne( + Relationship.self, + through: Self.reblog, + using: Self.relationship) static let showContentToggle = hasOne(StatusShowContentToggle.self) static let reblogShowContentToggle = hasOne( StatusShowContentToggle.self, diff --git a/DB/Sources/DB/Content/TimelineItemsInfo.swift b/DB/Sources/DB/Content/TimelineItemsInfo.swift index 4f21a96..833fd6a 100644 --- a/DB/Sources/DB/Content/TimelineItemsInfo.swift +++ b/DB/Sources/DB/Content/TimelineItemsInfo.swift @@ -40,12 +40,13 @@ extension TimelineItemsInfo { CollectionItem.status( .init(info: $0), .init(showContentToggled: $0.showContentToggled, - showAttachmentsToggled: $0.showAttachmentsToggled)) + showAttachmentsToggled: $0.showAttachmentsToggled), + $0.reblogRelationship ?? $0.relationship) } for loadMoreRecord in loadMoreRecords { guard let index = timelineItems.firstIndex(where: { - guard case let .status(status, _) = $0 else { return false } + guard case let .status(status, _, _) = $0 else { return false } return loadMoreRecord.afterStatusId > status.id }) else { continue } @@ -66,7 +67,8 @@ extension TimelineItemsInfo { .init(info: $0), .init(showContentToggled: $0.showContentToggled, showAttachmentsToggled: $0.showAttachmentsToggled, - isPinned: true)) + isPinned: true), + $0.reblogRelationship ?? $0.relationship) }), .init(items: timelineItems)] } else { diff --git a/DB/Sources/DB/Entities/CollectionItem.swift b/DB/Sources/DB/Entities/CollectionItem.swift index 8f8a029..3f7632d 100644 --- a/DB/Sources/DB/Entities/CollectionItem.swift +++ b/DB/Sources/DB/Entities/CollectionItem.swift @@ -3,9 +3,9 @@ import Mastodon public enum CollectionItem: Hashable { - case status(Status, StatusConfiguration) + case status(Status, StatusConfiguration, Relationship?) case loadMore(LoadMore) - case account(Account, AccountConfiguration) + case account(Account, AccountConfiguration, Relationship?) case notification(MastodonNotification, StatusConfiguration?) case conversation(Conversation) case tag(Tag) @@ -46,11 +46,11 @@ public extension CollectionItem { var itemId: Id? { switch self { - case let .status(status, _): + case let .status(status, _, _): return status.id case .loadMore: return nil - case let .account(account, _): + case let .account(account, _, _): return account.id case let .notification(notification, _): return notification.id diff --git a/Extensions/CollectionItem+Extensions.swift b/Extensions/CollectionItem+Extensions.swift index c4110a6..c2511af 100644 --- a/Extensions/CollectionItem+Extensions.swift +++ b/Extensions/CollectionItem+Extensions.swift @@ -35,13 +35,13 @@ extension CollectionItem { func estimatedHeight(width: CGFloat, identityContext: IdentityContext) -> CGFloat { switch self { - case let .status(status, configuration): + case let .status(status, configuration, _): return StatusView.estimatedHeight( width: width, identityContext: identityContext, status: status, configuration: configuration) - case let .account(account, configuration): + case let .account(account, configuration, _): return AccountView.estimatedHeight(width: width, account: account, configuration: configuration) case .loadMore: return LoadMoreView.estimatedHeight @@ -65,9 +65,9 @@ extension CollectionItem { func mediaPrefetchURLs(identityContext: IdentityContext) -> Set { switch self { - case let .status(status, _): + case let .status(status, _, _): return status.mediaPrefetchURLs(identityContext: identityContext) - case let .account(account, _): + case let .account(account, _, _): return account.mediaPrefetchURLs(identityContext: identityContext) case let .notification(notification, _): var urls = notification.account.mediaPrefetchURLs(identityContext: identityContext) diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift index 8c8aeb8..7d8fcef 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift @@ -9,6 +9,7 @@ import MastodonAPI public struct AccountListService { public let sections: AnyPublisher<[CollectionSection], Error> public let nextPageMaxId: AnyPublisher + public let accountIdsForRelationships: AnyPublisher, Never> public let navigationService: NavigationService public let canRefresh = false @@ -18,6 +19,7 @@ public struct AccountListService { private let contentDatabase: ContentDatabase private let titleComponents: [String]? private let nextPageMaxIdSubject = PassthroughSubject() + private let accountIdsForRelationshipsSubject = PassthroughSubject, Never>() init(endpoint: AccountsEndpoint, mastodonAPIClient: MastodonAPIClient, @@ -28,9 +30,11 @@ public struct AccountListService { self.contentDatabase = contentDatabase self.titleComponents = titleComponents sections = accountsSubject - .map { [.init(items: $0.map { CollectionItem.account($0, endpoint.configuration) })] } + .map { [.init(items: $0.map { CollectionItem.account($0, endpoint.configuration, nil) })] } // TODO: revisit + .removeDuplicates() .eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() + accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } } @@ -51,6 +55,7 @@ extension AccountListService: CollectionService { guard let maxId = $0.info.maxId else { return } nextPageMaxIdSubject.send(maxId) + accountIdsForRelationshipsSubject.send(Set($0.result.map(\.id))) }) .flatMap { contentDatabase.insert(accounts: $0.result) } .ignoreOutput() diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift index 0523d8d..a7627c7 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift @@ -8,24 +8,17 @@ import MastodonAPI public struct AccountService { public let account: Account - public let relationship: Relationship? - public let identityProofs: [IdentityProof] - public let featuredTags: [FeaturedTag] public let navigationService: NavigationService private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase public init(account: Account, - relationship: Relationship? = nil, identityProofs: [IdentityProof] = [], featuredTags: [FeaturedTag] = [], mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.account = account - self.relationship = relationship - self.identityProofs = identityProofs - self.featuredTags = featuredTags navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase diff --git a/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift b/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift index 425d9bc..e6bf355 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/CollectionService.swift @@ -6,6 +6,7 @@ import Mastodon public protocol CollectionService { var sections: AnyPublisher<[CollectionSection], Error> { get } var nextPageMaxId: AnyPublisher { get } + var accountIdsForRelationships: AnyPublisher, Never> { get } var preferLastPresentIdOverNextPageMaxId: Bool { get } var canRefresh: Bool { get } var title: AnyPublisher { get } @@ -18,6 +19,8 @@ public protocol CollectionService { extension CollectionService { public var nextPageMaxId: AnyPublisher { Empty().eraseToAnyPublisher() } + public var accountIdsForRelationships: AnyPublisher, Never> { Empty().eraseToAnyPublisher() } + public var preferLastPresentIdOverNextPageMaxId: Bool { false } public var canRefresh: Bool { true } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index e1a623b..2733ecc 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -114,6 +114,12 @@ public extension IdentityService { .eraseToAnyPublisher() } + func requestRelationships(ids: Set) -> AnyPublisher { + mastodonAPIClient.request(RelationshipsEndpoint.relationships(ids: Array(ids))) + .flatMap(contentDatabase.insert(relationships:)) + .eraseToAnyPublisher() + } + func getMarker(_ markerTimeline: Marker.Timeline) -> AnyPublisher { mastodonAPIClient.request(MarkersEndpoint.get([markerTimeline])) .compactMap { $0[markerTimeline.rawValue] } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift index 4725a3e..e3b098f 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift @@ -7,7 +7,7 @@ import Mastodon import MastodonAPI public struct ProfileService { - public let accountServicePublisher: AnyPublisher + public let profilePublisher: AnyPublisher private let id: Account.Id private let mastodonAPIClient: MastodonAPIClient @@ -34,26 +34,16 @@ public struct ProfileService { self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase - var accountPublisher = contentDatabase.profilePublisher(id: id) + var profilePublisher = contentDatabase.profilePublisher(id: id) if let account = account { - accountPublisher = accountPublisher + profilePublisher = profilePublisher .merge(with: Just(Profile(account: account)).setFailureType(to: Error.self)) .removeDuplicates() .eraseToAnyPublisher() } - accountServicePublisher = accountPublisher - .map { - AccountService( - account: $0.account, - relationship: $0.relationship, - identityProofs: $0.identityProofs, - featuredTags: $0.featuredTags, - mastodonAPIClient: mastodonAPIClient, - contentDatabase: contentDatabase) - } - .eraseToAnyPublisher() + self.profilePublisher = profilePublisher } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift index e47826b..a0f7a7d 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift @@ -10,6 +10,7 @@ public struct TimelineService { public let sections: AnyPublisher<[CollectionSection], Error> public let navigationService: NavigationService public let nextPageMaxId: AnyPublisher + public let accountIdsForRelationships: AnyPublisher, Never> public let title: AnyPublisher public let titleLocalizationComponents: AnyPublisher<[String], Never> @@ -17,6 +18,7 @@ public struct TimelineService { private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase private let nextPageMaxIdSubject = PassthroughSubject() + private let accountIdsForRelationshipsSubject = PassthroughSubject, Never>() init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.timeline = timeline @@ -25,6 +27,7 @@ public struct TimelineService { sections = contentDatabase.timelinePublisher(timeline) navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() + accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() switch timeline { case let .list(list): @@ -66,6 +69,10 @@ extension TimelineService: CollectionService { if let maxId = $0.info.maxId { nextPageMaxIdSubject.send(maxId) } + + accountIdsForRelationshipsSubject.send( + Set($0.result.map(\.account.id)) + .union(Set($0.result.compactMap(\.reblog?.account.id)))) }) .flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) } .eraseToAnyPublisher() diff --git a/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift b/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift index 6060158..ecbaedb 100644 --- a/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/AccountViewModel.swift @@ -8,6 +8,9 @@ import ServiceLayer public final class AccountViewModel: ObservableObject { public let identityContext: IdentityContext public internal(set) var configuration = CollectionItem.AccountConfiguration.withNote + public internal(set) var relationship: Relationship? + public internal(set) var identityProofs = [IdentityProof]() + public internal(set) var featuredTags = [FeaturedTag]() private let accountService: AccountService private let eventsSubject: PassthroughSubject, Never> @@ -44,12 +47,6 @@ public extension AccountViewModel { var isLocked: Bool { accountService.account.locked } - var relationship: Relationship? { accountService.relationship } - - var identityProofs: [IdentityProof] { accountService.identityProofs } - - var featuredTags: [FeaturedTag] { accountService.featuredTags } - var fields: [Account.Field] { accountService.account.fields } var note: NSAttributedString { accountService.account.note.attributed } diff --git a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift index 5edb445..d42b382 100644 --- a/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/CollectionItemsViewModel.swift @@ -42,6 +42,13 @@ public class CollectionItemsViewModel: ObservableObject { .sink { [weak self] in self?.nextPageMaxId = $0 } .store(in: &cancellables) + collectionService.accountIdsForRelationships + .filter { !$0.isEmpty } + .flatMap(identityContext.service.requestRelationships(ids:)) + .catch { _ in Empty().setFailureType(to: Never.self) } + .sink { _ in } + .store(in: &cancellables) + if let markerTimeline = collectionService.markerTimeline { shouldRestorePositionOfLocalLastReadId = identityContext.appPreferences.positionBehavior(markerTimeline: markerTimeline) == .rememberPosition @@ -134,14 +141,14 @@ extension CollectionItemsViewModel: CollectionViewModel { let item = lastUpdate.sections[indexPath.section].items[indexPath.item] switch item { - case let .status(status, _): + case let .status(status, _, relationship): send(event: .navigation(.collection(collectionService .navigationService .contextService(id: status.displayStatus.id)))) case let .loadMore(loadMore): lastSelectedLoadMore = loadMore (viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loadMore() - case let .account(account, _): + case let .account(account, _, relationship): send(event: .navigation(.profile(collectionService .navigationService .profileService(account: account)))) @@ -182,7 +189,7 @@ extension CollectionItemsViewModel: CollectionViewModel { public func canSelect(indexPath: IndexPath) -> Bool { switch lastUpdate.sections[indexPath.section].items[indexPath.item] { - case let .status(_, configuration): + case let .status(_, configuration, _): return !configuration.isContextParent case .loadMore: return !((viewModel(indexPath: indexPath) as? LoadMoreViewModel)?.loading ?? false) @@ -197,7 +204,7 @@ extension CollectionItemsViewModel: CollectionViewModel { let cachedViewModel = viewModelCache[item] switch item { - case let .status(status, configuration): + case let .status(status, configuration, relationship): let viewModel: StatusViewModel if let cachedViewModel = cachedViewModel as? StatusViewModel { @@ -211,6 +218,7 @@ extension CollectionItemsViewModel: CollectionViewModel { } viewModel.configuration = configuration + viewModel.accountViewModel.relationship = relationship return viewModel case let .loadMore(loadMore): @@ -225,7 +233,7 @@ extension CollectionItemsViewModel: CollectionViewModel { viewModelCache[item] = viewModel return viewModel - case let .account(account, configuration): + case let .account(account, configuration, relationship): let viewModel: AccountViewModel if let cachedViewModel = cachedViewModel as? AccountViewModel { @@ -239,6 +247,7 @@ extension CollectionItemsViewModel: CollectionViewModel { } viewModel.configuration = configuration + viewModel.relationship = relationship return viewModel case let .notification(notification, statusConfiguration): @@ -302,7 +311,7 @@ extension CollectionItemsViewModel: CollectionViewModel { public func toggleExpandAll() { let statusIds = Set(lastUpdate.sections.map(\.items).reduce([], +).compactMap { item -> Status.Id? in - guard case let .status(status, _) = item else { return nil } + guard case let .status(status, _, _) = item else { return nil } return status.id }) @@ -388,7 +397,7 @@ private extension CollectionItemsViewModel { if collectionService is ContextService, lastUpdate.sections.isEmpty || lastUpdate.sections.map(\.items.count) == [0, 1, 0], let contextParent = newItems.first(where: { - guard case let .status(_, configuration) = $0 else { return false } + guard case let .status(_, configuration, _) = $0 else { return false } return configuration.isContextParent // Maintain scroll position of parent after initial load of context }) { @@ -404,7 +413,7 @@ private extension CollectionItemsViewModel { let direction = (viewModelCache[item] as? LoadMoreViewModel)?.direction, direction == .up, let statusAfterLoadMore = items.first(where: { - guard case let .status(status, _) = $0 else { return false } + guard case let .status(status, _, _) = $0 else { return false } return status.id == loadMore.beforeStatusId }) { diff --git a/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift b/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift index 9733c3f..007c3fb 100644 --- a/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/ProfileViewModel.swift @@ -30,11 +30,19 @@ final public class ProfileViewModel { self.accountEventsSubject = accountEventsSubject - profileService.accountServicePublisher + profileService.profilePublisher .map { - AccountViewModel(accountService: $0, + let vm = AccountViewModel(accountService: identityContext.service + .navigationService + .accountService(account: $0.account), identityContext: identityContext, eventsSubject: accountEventsSubject) + + vm.relationship = $0.relationship + vm.identityProofs = $0.identityProofs + vm.featuredTags = $0.featuredTags + + return vm } .assignErrorsToAlertItem(to: \.alertItem, on: self) .assign(to: &$accountViewModel)