Long press to boost / favorite from other accounts

This commit is contained in:
Justin Mazzocchi 2021-03-21 12:56:42 -07:00
parent bfcb999b25
commit f3040eaad5
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
25 changed files with 236 additions and 60 deletions

View file

@ -169,10 +169,12 @@ public extension IdentityDatabase {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func authenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> { func authenticatedIdentitiesPublisher(excluding: Identity.Id) -> AnyPublisher<[Identity], Error> {
ValueObservation.tracking( ValueObservation.tracking(
IdentityInfo.request(IdentityRecord.order(IdentityRecord.Columns.lastUsedAt.desc)) IdentityInfo.request(IdentityRecord.order(IdentityRecord.Columns.lastUsedAt.desc))
.filter(IdentityRecord.Columns.authenticated == true && IdentityRecord.Columns.pending == false) .filter(IdentityRecord.Columns.authenticated == true
&& IdentityRecord.Columns.pending == false
&& IdentityRecord.Columns.id != excluding)
.fetchAll) .fetchAll)
.removeDuplicates() .removeDuplicates()
.publisher(in: databaseWriter) .publisher(in: databaseWriter)

View file

@ -60,6 +60,7 @@
"account.unnotify" = "Turn off notifications"; "account.unnotify" = "Turn off notifications";
"activity.open-in-default-browser" = "Open in default browser"; "activity.open-in-default-browser" = "Open in default browser";
"add" = "Add"; "add" = "Add";
"api-error.unable-to-fetch-remote-status" = "Unable to fetch remote status";
"apns-default-message" = "New notification"; "apns-default-message" = "New notification";
"app-icon.brutalist" = "Brutalist"; "app-icon.brutalist" = "Brutalist";
"app-icon.rainbow-brutalist" = "Rainbow Brutalist"; "app-icon.rainbow-brutalist" = "Rainbow Brutalist";

View file

@ -9,3 +9,8 @@ public struct APIError: Error, Codable {
extension APIError: LocalizedError { extension APIError: LocalizedError {
public var errorDescription: String? { error } public var errorDescription: String? { error }
} }
public extension APIError {
static let unableToFetchRemoteStatus =
Self(error: NSLocalizedString("api-error.unable-to-fetch-remote-status", comment: ""))
}

View file

@ -0,0 +1,16 @@
// Copyright © 2021 Metabolist. All rights reserved.
import MastodonAPI
import Secrets
extension MastodonAPIClient {
static func forIdentity(id: Identity.Id, environment: AppEnvironment) throws -> Self {
let secrets = Secrets(identityId: id, keychain: environment.keychain)
let client = Self(session: environment.session, instanceURL: try secrets.getInstanceURL())
client.accessToken = try secrets.getAccessToken()
return client
}
}

View file

@ -22,6 +22,7 @@ public struct AccountListService {
private let accountIdsForRelationshipsSubject = PassthroughSubject<Set<Account.Id>, Never>() private let accountIdsForRelationshipsSubject = PassthroughSubject<Set<Account.Id>, Never>()
init(endpoint: AccountsEndpoint, init(endpoint: AccountsEndpoint,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient, mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase, contentDatabase: ContentDatabase,
titleComponents: [String]? = nil) { titleComponents: [String]? = nil) {
@ -32,7 +33,9 @@ public struct AccountListService {
sections = contentDatabase.accountListPublisher(id: listId, configuration: endpoint.configuration) sections = contentDatabase.accountListPublisher(id: listId, configuration: endpoint.configuration)
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
} }

View file

@ -10,16 +10,21 @@ public struct AccountService {
public let account: Account public let account: Account
public let navigationService: NavigationService public let navigationService: NavigationService
private let environment: AppEnvironment
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
public init(account: Account, public init(account: Account,
identityProofs: [IdentityProof] = [], identityProofs: [IdentityProof] = [],
featuredTags: [FeaturedTag] = [], featuredTags: [FeaturedTag] = [],
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient, mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) { contentDatabase: ContentDatabase) {
self.account = account self.account = account
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
self.environment = environment
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
} }
@ -136,6 +141,7 @@ public extension AccountService {
func followingService() -> AccountListService { func followingService() -> AccountListService {
AccountListService( AccountListService(
endpoint: .accountsFollowing(id: account.id), endpoint: .accountsFollowing(id: account.id),
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase, contentDatabase: contentDatabase,
titleComponents: ["account.followed-by-%@", "@".appending(account.acct)]) titleComponents: ["account.followed-by-%@", "@".appending(account.acct)])
@ -144,6 +150,7 @@ public extension AccountService {
func followersService() -> AccountListService { func followersService() -> AccountListService {
AccountListService( AccountListService(
endpoint: .accountsFollowers(id: account.id), endpoint: .accountsFollowers(id: account.id),
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase, contentDatabase: contentDatabase,
titleComponents: ["account.%@-followers", "@".appending(account.acct)]) titleComponents: ["account.%@-followers", "@".appending(account.acct)])

View file

@ -39,10 +39,6 @@ public extension AllIdentitiesService {
database.immediateMostRecentlyUsedIdentityIdPublisher() database.immediateMostRecentlyUsedIdentityIdPublisher()
} }
func authenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> {
database.authenticatedIdentitiesPublisher()
}
func mostRecentAuthenticatedIdentity() throws -> Identity? { func mostRecentAuthenticatedIdentity() throws -> Identity? {
try database.mostRecentAuthenticatedIdentity() try database.mostRecentAuthenticatedIdentity()
} }

View file

@ -14,12 +14,17 @@ public struct ContextService {
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
init(id: Status.Id, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(id: Status.Id,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) {
self.id = id self.id = id
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
sections = contentDatabase.contextPublisher(id: id) sections = contentDatabase.contextPublisher(id: id)
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
} }

View file

@ -12,9 +12,13 @@ public struct ConversationService {
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
init(conversation: Conversation, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(conversation: Conversation,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) {
self.conversation = conversation self.conversation = conversation
self.navigationService = NavigationService( self.navigationService = NavigationService(
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient

View file

@ -15,14 +15,17 @@ public struct ConversationsService {
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>() private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
sections = contentDatabase.conversationsPublisher() sections = contentDatabase.conversationsPublisher()
.map { [.init(items: $0.map(CollectionItem.conversation))] } .map { [.init(items: $0.map(CollectionItem.conversation))] }
.eraseToAnyPublisher() .eraseToAnyPublisher()
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(
environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
} }

View file

@ -12,10 +12,12 @@ public struct ExploreService {
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
} }

View file

@ -39,6 +39,7 @@ public struct IdentityService {
keychain: environment.keychain) keychain: environment.keychain)
navigationService = NavigationService( navigationService = NavigationService(
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
@ -98,6 +99,10 @@ public extension IdentityService {
identityDatabase.recentIdentitiesPublisher(excluding: id) identityDatabase.recentIdentitiesPublisher(excluding: id)
} }
func otherAuthenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> {
identityDatabase.authenticatedIdentitiesPublisher(excluding: id)
}
func refreshLists() -> AnyPublisher<Never, Error> { func refreshLists() -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(ListsEndpoint.lists) mastodonAPIClient.request(ListsEndpoint.lists)
.flatMap(contentDatabase.setLists(_:)) .flatMap(contentDatabase.setLists(_:))
@ -249,6 +254,7 @@ public extension IdentityService {
.map { _ in .map { _ in
NotificationService( NotificationService(
notification: notification, notification: notification,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
@ -259,27 +265,31 @@ public extension IdentityService {
func service(accountList: AccountsEndpoint, titleComponents: [String]? = nil) -> AccountListService { func service(accountList: AccountsEndpoint, titleComponents: [String]? = nil) -> AccountListService {
AccountListService( AccountListService(
endpoint: accountList, endpoint: accountList,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase, contentDatabase: contentDatabase,
titleComponents: titleComponents) titleComponents: titleComponents)
} }
func exploreService() -> ExploreService { func exploreService() -> ExploreService {
ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) ExploreService(environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
} }
func searchService() -> SearchService { func searchService() -> SearchService {
SearchService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) SearchService(environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
} }
func notificationsService(excludeTypes: Set<MastodonNotification.NotificationType>) -> NotificationsService { func notificationsService(excludeTypes: Set<MastodonNotification.NotificationType>) -> NotificationsService {
NotificationsService(excludeTypes: excludeTypes, NotificationsService(excludeTypes: excludeTypes,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
func conversationsService() -> ConversationsService { func conversationsService() -> ConversationsService {
ConversationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) ConversationsService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
func domainBlocksService() -> DomainBlocksService { func domainBlocksService() -> DomainBlocksService {

View file

@ -17,11 +17,16 @@ public enum Navigation {
} }
public struct NavigationService { public struct NavigationService {
private let environment: AppEnvironment
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let status: Status? private let status: Status?
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase, status: Status? = nil) { init(environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase,
status: Status? = nil) {
self.environment = environment
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
self.status = status self.status = status
@ -35,6 +40,7 @@ public extension NavigationService {
.collection( .collection(
TimelineService( TimelineService(
timeline: .tag(tag), timeline: .tag(tag),
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase))) contentDatabase: contentDatabase)))
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -52,26 +58,38 @@ public extension NavigationService {
} }
func contextService(id: Status.Id) -> ContextService { func contextService(id: Status.Id) -> ContextService {
ContextService(id: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) ContextService(id: id, environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
func profileService(id: Account.Id) -> ProfileService { func profileService(id: Account.Id) -> ProfileService {
ProfileService(id: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) ProfileService(id: id,
environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
func profileService(account: Account, relationship: Relationship? = nil) -> ProfileService { func profileService(account: Account, relationship: Relationship? = nil) -> ProfileService {
ProfileService(account: account, ProfileService(account: account,
relationship: relationship, relationship: relationship,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
func statusService(status: Status) -> StatusService { func statusService(status: Status) -> StatusService {
StatusService(status: status, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) StatusService(environment: environment,
status: status,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
func accountService(account: Account) -> AccountService { func accountService(account: Account) -> AccountService {
AccountService(account: account, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) AccountService(account: account,
environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
func loadMoreService(loadMore: LoadMore) -> LoadMoreService { func loadMoreService(loadMore: LoadMore) -> LoadMoreService {
@ -81,6 +99,7 @@ public extension NavigationService {
func notificationService(notification: MastodonNotification) -> NotificationService { func notificationService(notification: MastodonNotification) -> NotificationService {
NotificationService( NotificationService(
notification: notification, notification: notification,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
@ -88,12 +107,16 @@ public extension NavigationService {
func conversationService(conversation: Conversation) -> ConversationService { func conversationService(conversation: Conversation) -> ConversationService {
ConversationService( ConversationService(
conversation: conversation, conversation: conversation,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
func timelineService(timeline: Timeline) -> TimelineService { func timelineService(timeline: Timeline) -> TimelineService {
TimelineService(timeline: timeline, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) TimelineService(timeline: timeline,
environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
} }
@ -132,6 +155,7 @@ private extension NavigationService {
return .collection( return .collection(
TimelineService( TimelineService(
timeline: .tag(tag.name), timeline: .tag(tag.name),
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)) contentDatabase: contentDatabase))
} else if let account = results.accounts.first { } else if let account = results.accounts.first {

View file

@ -12,9 +12,13 @@ public struct NotificationService {
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
init(notification: MastodonNotification, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(notification: MastodonNotification,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) {
self.notification = notification self.notification = notification
self.navigationService = NavigationService( self.navigationService = NavigationService(
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase, contentDatabase: contentDatabase,
status: nil) status: nil)

View file

@ -18,6 +18,7 @@ public struct NotificationsService {
private let nextPageMaxIdSubject: CurrentValueSubject<String, Never> private let nextPageMaxIdSubject: CurrentValueSubject<String, Never>
init(excludeTypes: Set<MastodonNotification.NotificationType>, init(excludeTypes: Set<MastodonNotification.NotificationType>,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient, mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) { contentDatabase: ContentDatabase) {
self.excludeTypes = excludeTypes self.excludeTypes = excludeTypes
@ -37,7 +38,9 @@ public struct NotificationsService {
}) })
.eraseToAnyPublisher() .eraseToAnyPublisher()
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
} }
} }

View file

@ -10,25 +10,32 @@ public struct ProfileService {
public let profilePublisher: AnyPublisher<Profile, Error> public let profilePublisher: AnyPublisher<Profile, Error>
private let id: Account.Id private let id: Account.Id
private let environment: AppEnvironment
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
init(account: Account, init(account: Account,
relationship: Relationship?, relationship: Relationship?,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient, mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) { contentDatabase: ContentDatabase) {
self.init( self.init(
id: account.id, id: account.id,
account: account, account: account,
relationship: relationship, relationship: relationship,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
init(id: Account.Id, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(id: Account.Id,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) {
self.init(id: id, self.init(id: id,
account: nil, account: nil,
relationship: nil, relationship: nil,
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
@ -37,9 +44,11 @@ public struct ProfileService {
id: Account.Id, id: Account.Id,
account: Account?, account: Account?,
relationship: Relationship?, relationship: Relationship?,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient, mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) { contentDatabase: ContentDatabase) {
self.id = id self.id = id
self.environment = environment
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
@ -60,6 +69,7 @@ public extension ProfileService {
func timelineService(profileCollection: ProfileCollection) -> TimelineService { func timelineService(profileCollection: ProfileCollection) -> TimelineService {
TimelineService( TimelineService(
timeline: .profile(accountId: id, profileCollection: profileCollection), timeline: .profile(accountId: id, profileCollection: profileCollection),
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }

View file

@ -16,11 +16,13 @@ public struct SearchService {
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>() private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
private let resultsSubject = PassthroughSubject<(Results, Search), Error>() private let resultsSubject = PassthroughSubject<(Results, Search), Error>()
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
sections = resultsSubject.scan((.empty, nil)) { sections = resultsSubject.scan((.empty, nil)) {
let (results, search) = $1 let (results, search) = $1

View file

@ -9,15 +9,21 @@ import MastodonAPI
public struct StatusService { public struct StatusService {
public let status: Status public let status: Status
public let navigationService: NavigationService public let navigationService: NavigationService
private let environment: AppEnvironment
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
init(status: Status, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(environment: AppEnvironment,
status: Status,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) {
self.status = status self.status = status
self.navigationService = NavigationService( self.navigationService = NavigationService(
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase, contentDatabase: contentDatabase,
status: status.displayStatus) status: status.displayStatus)
self.environment = environment
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
} }
@ -32,21 +38,29 @@ public extension StatusService {
contentDatabase.toggleShowAttachments(id: status.displayStatus.id) contentDatabase.toggleShowAttachments(id: status.displayStatus.id)
} }
func toggleReblogged() -> AnyPublisher<Never, Error> { func toggleReblogged(identityId: Identity.Id?) -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(status.displayStatus.reblogged if let identityId = identityId {
return request(identityId: identityId, endpointClosure: StatusEndpoint.reblog(id:))
} else {
return mastodonAPIClient.request(status.displayStatus.reblogged
? StatusEndpoint.unreblog(id: status.displayStatus.id) ? StatusEndpoint.unreblog(id: status.displayStatus.id)
: StatusEndpoint.reblog(id: status.displayStatus.id)) : StatusEndpoint.reblog(id: status.displayStatus.id))
.flatMap(contentDatabase.insert(status:)) .flatMap(contentDatabase.insert(status:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
}
func toggleFavorited() -> AnyPublisher<Never, Error> { func toggleFavorited(identityId: Identity.Id?) -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(status.displayStatus.favourited if let identityId = identityId {
return request(identityId: identityId, endpointClosure: StatusEndpoint.favourite(id:))
} else {
return mastodonAPIClient.request(status.displayStatus.favourited
? StatusEndpoint.unfavourite(id: status.displayStatus.id) ? StatusEndpoint.unfavourite(id: status.displayStatus.id)
: StatusEndpoint.favourite(id: status.displayStatus.id)) : StatusEndpoint.favourite(id: status.displayStatus.id))
.flatMap(contentDatabase.insert(status:)) .flatMap(contentDatabase.insert(status:))
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
}
func toggleBookmarked() -> AnyPublisher<Never, Error> { func toggleBookmarked() -> AnyPublisher<Never, Error> {
mastodonAPIClient.request(status.displayStatus.bookmarked mastodonAPIClient.request(status.displayStatus.bookmarked
@ -84,7 +98,8 @@ public extension StatusService {
if let inReplyToId = status.displayStatus.inReplyToId { if let inReplyToId = status.displayStatus.inReplyToId {
inReplyToPublisher = mastodonAPIClient.request(StatusEndpoint.status(id: inReplyToId)) inReplyToPublisher = mastodonAPIClient.request(StatusEndpoint.status(id: inReplyToId))
.map { .map {
Self(status: $0, Self(environment: environment,
status: $0,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) as Self? contentDatabase: contentDatabase) as Self?
} }
@ -103,6 +118,7 @@ public extension StatusService {
func rebloggedByService() -> AccountListService { func rebloggedByService() -> AccountListService {
AccountListService( AccountListService(
endpoint: .rebloggedBy(id: status.id), endpoint: .rebloggedBy(id: status.id),
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
@ -110,6 +126,7 @@ public extension StatusService {
func favoritedByService() -> AccountListService { func favoritedByService() -> AccountListService {
AccountListService( AccountListService(
endpoint: .favouritedBy(id: status.id), endpoint: .favouritedBy(id: status.id),
environment: environment,
mastodonAPIClient: mastodonAPIClient, mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase) contentDatabase: contentDatabase)
} }
@ -130,3 +147,28 @@ public extension StatusService {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }
private extension StatusService {
func request(identityId: Identity.Id,
endpointClosure: @escaping (Status.Id) -> StatusEndpoint) -> AnyPublisher<Never, Error> {
let client: MastodonAPIClient
do {
client = try MastodonAPIClient.forIdentity(id: identityId, environment: environment)
} catch {
return Fail(error: error).eraseToAnyPublisher()
}
return client
.request(ResultsEndpoint.search(.init(query: status.displayStatus.uri, resolve: true, limit: 1)))
.tryMap {
guard let id = $0.statuses.first?.id else { throw APIError.unableToFetchRemoteStatus }
return id
}
.flatMap { client.request(endpointClosure($0)) }
.flatMap { _ in mastodonAPIClient.request(StatusEndpoint.status(id: status.displayStatus.id)) }
.flatMap(contentDatabase.insert(status:))
.eraseToAnyPublisher()
}
}

View file

@ -21,12 +21,17 @@ public struct TimelineService {
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>() private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
private let accountIdsForRelationshipsSubject = PassthroughSubject<Set<Account.Id>, Never>() private let accountIdsForRelationshipsSubject = PassthroughSubject<Set<Account.Id>, Never>()
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(timeline: Timeline,
environment: AppEnvironment,
mastodonAPIClient: MastodonAPIClient,
contentDatabase: ContentDatabase) {
self.timeline = timeline self.timeline = timeline
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
sections = contentDatabase.timelinePublisher(timeline) sections = contentDatabase.timelinePublisher(timeline)
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(environment: environment,
mastodonAPIClient: mastodonAPIClient,
contentDatabase: contentDatabase)
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher()

View file

@ -81,6 +81,7 @@ public extension ReportViewModel {
static let preview = ReportViewModel( static let preview = ReportViewModel(
accountService: AccountService( accountService: AccountService(
account: .preview, account: .preview,
environment: environment,
mastodonAPIClient: .preview, mastodonAPIClient: .preview,
contentDatabase: .preview), contentDatabase: .preview),
identityContext: .preview) identityContext: .preview)
@ -90,6 +91,7 @@ public extension MuteViewModel {
static let preview = MuteViewModel( static let preview = MuteViewModel(
accountService: AccountService( accountService: AccountService(
account: .preview, account: .preview,
environment: environment,
mastodonAPIClient: .preview, mastodonAPIClient: .preview,
contentDatabase: .preview), contentDatabase: .preview),
identityContext: .preview) identityContext: .preview)

View file

@ -6,6 +6,7 @@ import ServiceLayer
public final class IdentityContext: ObservableObject { public final class IdentityContext: ObservableObject {
@Published private(set) public var identity: Identity @Published private(set) public var identity: Identity
@Published private(set) public var authenticatedOtherIdentities = [Identity]()
@Published public var appPreferences: AppPreferences @Published public var appPreferences: AppPreferences
let service: IdentityService let service: IdentityService
@ -19,6 +20,9 @@ public final class IdentityContext: ObservableObject {
DispatchQueue.main.async { DispatchQueue.main.async {
publisher.dropFirst().assign(to: &self.$identity) publisher.dropFirst().assign(to: &self.$identity)
service.otherAuthenticatedIdentitiesPublisher()
.replaceError(with: [])
.assign(to: &self.$authenticatedOtherIdentities)
} }
} }
} }

View file

@ -9,7 +9,6 @@ public final class NewStatusViewModel: ObservableObject {
@Published public var visibility: Status.Visibility @Published public var visibility: Status.Visibility
@Published public private(set) var compositionViewModels = [CompositionViewModel]() @Published public private(set) var compositionViewModels = [CompositionViewModel]()
@Published public private(set) var identityContext: IdentityContext @Published public private(set) var identityContext: IdentityContext
@Published public private(set) var authenticatedIdentities = [Identity]()
@Published public var canPost = false @Published public var canPost = false
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
@Published public private(set) var postingState = PostingState.composing @Published public private(set) var postingState = PostingState.composing
@ -87,14 +86,6 @@ public final class NewStatusViewModel: ObservableObject {
} }
compositionViewModels = [compositionViewModel] compositionViewModels = [compositionViewModel]
allIdentitiesService.authenticatedIdentitiesPublisher()
.assignErrorsToAlertItem(to: \.alertItem, on: self)
.combineLatest($identityContext)
.map { authenticatedIdentities, currentIdentity in
authenticatedIdentities.filter { $0.id != currentIdentity.identity.id }
}
.assign(to: &$authenticatedIdentities)
$compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) } $compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) }
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring .receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
.compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) } .compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) }

View file

@ -255,16 +255,16 @@ public extension StatusViewModel {
.eraseToAnyPublisher()) .eraseToAnyPublisher())
} }
func toggleReblogged() { func toggleReblogged(identityId: Identity.Id? = nil) {
eventsSubject.send( eventsSubject.send(
statusService.toggleReblogged() statusService.toggleReblogged(identityId: identityId)
.map { _ in .ignorableOutput } .map { _ in .ignorableOutput }
.eraseToAnyPublisher()) .eraseToAnyPublisher())
} }
func toggleFavorited() { func toggleFavorited(identityId: Identity.Id? = nil) {
eventsSubject.send( eventsSubject.send(
statusService.toggleFavorited() statusService.toggleFavorited(identityId: identityId)
.map { _ in .ignorableOutput } .map { _ in .ignorableOutput }
.eraseToAnyPublisher()) .eraseToAnyPublisher())
} }

View file

@ -74,7 +74,8 @@ private extension CompositionView {
avatarImageView.isUserInteractionEnabled = true avatarImageView.isUserInteractionEnabled = true
changeIdentityButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) changeIdentityButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted)
changeIdentityButton.showsMenuAsPrimaryAction = true changeIdentityButton.showsMenuAsPrimaryAction = true
changeIdentityButton.menu = changeIdentityMenu(identities: parentViewModel.authenticatedIdentities) changeIdentityButton.menu =
changeIdentityMenu(identities: parentViewModel.identityContext.authenticatedOtherIdentities)
let stackView = UIStackView() let stackView = UIStackView()
@ -205,7 +206,7 @@ private extension CompositionView {
} }
.store(in: &cancellables) .store(in: &cancellables)
parentViewModel.$authenticatedIdentities parentViewModel.identityContext.$authenticatedOtherIdentities
.sink { [weak self] in self?.changeIdentityButton.menu = self?.changeIdentityMenu(identities: $0) } .sink { [weak self] in self?.changeIdentityButton.menu = self?.changeIdentityMenu(identities: $0) }
.store(in: &cancellables) .store(in: &cancellables)

View file

@ -616,9 +616,11 @@ private extension StatusView {
setReblogButtonColor(reblogged: viewModel.reblogged) setReblogButtonColor(reblogged: viewModel.reblogged)
reblogButton.isEnabled = viewModel.canBeReblogged && isAuthenticated reblogButton.isEnabled = viewModel.canBeReblogged && isAuthenticated
reblogButton.menu = authenticatedIdentitiesMenu { viewModel.toggleReblogged(identityId: $0.id) }
setFavoriteButtonColor(favorited: viewModel.favorited) setFavoriteButtonColor(favorited: viewModel.favorited)
favoriteButton.isEnabled = isAuthenticated favoriteButton.isEnabled = isAuthenticated
favoriteButton.menu = authenticatedIdentitiesMenu { viewModel.toggleFavorited(identityId: $0.id) }
shareButton.tag = viewModel.sharingURL?.hashValue ?? 0 shareButton.tag = viewModel.sharingURL?.hashValue ?? 0
@ -1127,6 +1129,38 @@ private extension StatusView {
return actions return actions
} }
func authenticatedIdentitiesMenu(action: @escaping (Identity) -> Void) -> UIMenu {
let imageTransformer = SDImageRoundCornerTransformer(
radius: .greatestFiniteMagnitude,
corners: .allCorners,
borderWidth: 0,
borderColor: nil)
return UIMenu(children: statusConfiguration.viewModel
.identityContext
.authenticatedOtherIdentities.map { identity in
UIDeferredMenuElement { completion in
let menuItemAction = UIAction(title: identity.handle) { _ in
action(identity)
}
if let image = identity.image {
SDWebImageManager.shared.loadImage(
with: image,
options: [.transformAnimatedImage],
context: [.imageTransformer: imageTransformer],
progress: nil) { (image, _, _, _, _, _) in
menuItemAction.image = image
completion([menuItemAction])
}
} else {
completion([menuItemAction])
}
}
})
}
} }
private extension UIButton { private extension UIButton {