This commit is contained in:
Justin Mazzocchi 2021-02-07 17:46:51 -08:00
parent 6bbfb2d06e
commit b32a85aebc
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
16 changed files with 86 additions and 52 deletions

View File

@ -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 {

View File

@ -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) }

View File

@ -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<T: DerivableRequest>(_ 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))

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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<URL> {
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)

View File

@ -9,6 +9,7 @@ import MastodonAPI
public struct AccountListService {
public let sections: AnyPublisher<[CollectionSection], Error>
public let nextPageMaxId: AnyPublisher<String, Never>
public let accountIdsForRelationships: AnyPublisher<Set<Account.Id>, 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<String, Never>()
private let accountIdsForRelationshipsSubject = PassthroughSubject<Set<Account.Id>, 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()

View File

@ -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

View File

@ -6,6 +6,7 @@ import Mastodon
public protocol CollectionService {
var sections: AnyPublisher<[CollectionSection], Error> { get }
var nextPageMaxId: AnyPublisher<String, Never> { get }
var accountIdsForRelationships: AnyPublisher<Set<Account.Id>, Never> { get }
var preferLastPresentIdOverNextPageMaxId: Bool { get }
var canRefresh: Bool { get }
var title: AnyPublisher<String, Never> { get }
@ -18,6 +19,8 @@ public protocol CollectionService {
extension CollectionService {
public var nextPageMaxId: AnyPublisher<String, Never> { Empty().eraseToAnyPublisher() }
public var accountIdsForRelationships: AnyPublisher<Set<Account.Id>, Never> { Empty().eraseToAnyPublisher() }
public var preferLastPresentIdOverNextPageMaxId: Bool { false }
public var canRefresh: Bool { true }

View File

@ -114,6 +114,12 @@ public extension IdentityService {
.eraseToAnyPublisher()
}
func requestRelationships(ids: Set<Account.Id>) -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(RelationshipsEndpoint.relationships(ids: Array(ids)))
.flatMap(contentDatabase.insert(relationships:))
.eraseToAnyPublisher()
}
func getMarker(_ markerTimeline: Marker.Timeline) -> AnyPublisher<Marker, Error> {
mastodonAPIClient.request(MarkersEndpoint.get([markerTimeline]))
.compactMap { $0[markerTimeline.rawValue] }

View File

@ -7,7 +7,7 @@ import Mastodon
import MastodonAPI
public struct ProfileService {
public let accountServicePublisher: AnyPublisher<AccountService, Error>
public let profilePublisher: AnyPublisher<Profile, Error>
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
}
}

View File

@ -10,6 +10,7 @@ public struct TimelineService {
public let sections: AnyPublisher<[CollectionSection], Error>
public let navigationService: NavigationService
public let nextPageMaxId: AnyPublisher<String, Never>
public let accountIdsForRelationships: AnyPublisher<Set<Account.Id>, Never>
public let title: AnyPublisher<String, Never>
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<String, Never>()
private let accountIdsForRelationshipsSubject = PassthroughSubject<Set<Account.Id>, 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()

View File

@ -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<AnyPublisher<CollectionItemEvent, Error>, 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 }

View File

@ -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
}) {

View File

@ -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)