From f3040eaad51843bc0c928cb20b96b2a2511bd9d8 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 21 Mar 2021 12:56:42 -0700 Subject: [PATCH] Long press to boost / favorite from other accounts --- DB/Sources/DB/Identity/IdentityDatabase.swift | 6 +- Localizations/en.lproj/Localizable.strings | 1 + .../Sources/Mastodon/Entities/APIError.swift | 5 ++ .../MastodonAPIClient+Extensions.swift | 16 +++++ .../Services/AccountListService.swift | 5 +- .../Services/AccountService.swift | 9 ++- .../Services/AllIdentitiesService.swift | 4 -- .../Services/ContextService.swift | 9 ++- .../Services/ConversationService.swift | 6 +- .../Services/ConversationsService.swift | 7 +- .../Services/ExploreService.swift | 6 +- .../Services/IdentityService.swift | 16 ++++- .../Services/NavigationService.swift | 36 ++++++++-- .../Services/NotificationService.swift | 6 +- .../Services/NotificationsService.swift | 5 +- .../Services/ProfileService.swift | 12 +++- .../ServiceLayer/Services/SearchService.swift | 6 +- .../ServiceLayer/Services/StatusService.swift | 70 +++++++++++++++---- .../Services/TimelineService.swift | 9 ++- .../PreviewViewModels/PreviewViewModels.swift | 2 + .../ViewModels/Entities/IdentityContext.swift | 4 ++ .../View Models/NewStatusViewModel.swift | 9 --- .../View Models/StatusViewModel.swift | 8 +-- Views/UIKit/CompositionView.swift | 5 +- Views/UIKit/Content Views/StatusView.swift | 34 +++++++++ 25 files changed, 236 insertions(+), 60 deletions(-) create mode 100644 ServiceLayer/Sources/ServiceLayer/Extensions/MastodonAPIClient+Extensions.swift diff --git a/DB/Sources/DB/Identity/IdentityDatabase.swift b/DB/Sources/DB/Identity/IdentityDatabase.swift index 0016073..f37a88b 100644 --- a/DB/Sources/DB/Identity/IdentityDatabase.swift +++ b/DB/Sources/DB/Identity/IdentityDatabase.swift @@ -169,10 +169,12 @@ public extension IdentityDatabase { .eraseToAnyPublisher() } - func authenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> { + func authenticatedIdentitiesPublisher(excluding: Identity.Id) -> AnyPublisher<[Identity], Error> { ValueObservation.tracking( 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) .removeDuplicates() .publisher(in: databaseWriter) diff --git a/Localizations/en.lproj/Localizable.strings b/Localizations/en.lproj/Localizable.strings index 989f7c6..ec0c1f9 100644 --- a/Localizations/en.lproj/Localizable.strings +++ b/Localizations/en.lproj/Localizable.strings @@ -60,6 +60,7 @@ "account.unnotify" = "Turn off notifications"; "activity.open-in-default-browser" = "Open in default browser"; "add" = "Add"; +"api-error.unable-to-fetch-remote-status" = "Unable to fetch remote status"; "apns-default-message" = "New notification"; "app-icon.brutalist" = "Brutalist"; "app-icon.rainbow-brutalist" = "Rainbow Brutalist"; diff --git a/Mastodon/Sources/Mastodon/Entities/APIError.swift b/Mastodon/Sources/Mastodon/Entities/APIError.swift index e360df1..cb2c5a6 100644 --- a/Mastodon/Sources/Mastodon/Entities/APIError.swift +++ b/Mastodon/Sources/Mastodon/Entities/APIError.swift @@ -9,3 +9,8 @@ public struct APIError: Error, Codable { extension APIError: LocalizedError { public var errorDescription: String? { error } } + +public extension APIError { + static let unableToFetchRemoteStatus = + Self(error: NSLocalizedString("api-error.unable-to-fetch-remote-status", comment: "")) +} diff --git a/ServiceLayer/Sources/ServiceLayer/Extensions/MastodonAPIClient+Extensions.swift b/ServiceLayer/Sources/ServiceLayer/Extensions/MastodonAPIClient+Extensions.swift new file mode 100644 index 0000000..f760c42 --- /dev/null +++ b/ServiceLayer/Sources/ServiceLayer/Extensions/MastodonAPIClient+Extensions.swift @@ -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 + } +} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift index 39950d9..2fad17a 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountListService.swift @@ -22,6 +22,7 @@ public struct AccountListService { private let accountIdsForRelationshipsSubject = PassthroughSubject, Never>() init(endpoint: AccountsEndpoint, + environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase, titleComponents: [String]? = nil) { @@ -32,7 +33,9 @@ public struct AccountListService { sections = contentDatabase.accountListPublisher(id: listId, configuration: endpoint.configuration) nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift index 1a00f03..fd5108c 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AccountService.swift @@ -10,16 +10,21 @@ public struct AccountService { public let account: Account public let navigationService: NavigationService + private let environment: AppEnvironment private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase public init(account: Account, identityProofs: [IdentityProof] = [], featuredTags: [FeaturedTag] = [], + environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.account = account - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) + self.environment = environment self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase } @@ -136,6 +141,7 @@ public extension AccountService { func followingService() -> AccountListService { AccountListService( endpoint: .accountsFollowing(id: account.id), + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase, titleComponents: ["account.followed-by-%@", "@".appending(account.acct)]) @@ -144,6 +150,7 @@ public extension AccountService { func followersService() -> AccountListService { AccountListService( endpoint: .accountsFollowers(id: account.id), + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase, titleComponents: ["account.%@-followers", "@".appending(account.acct)]) diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift index fa7224a..e2a9b55 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift @@ -39,10 +39,6 @@ public extension AllIdentitiesService { database.immediateMostRecentlyUsedIdentityIdPublisher() } - func authenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> { - database.authenticatedIdentitiesPublisher() - } - func mostRecentAuthenticatedIdentity() throws -> Identity? { try database.mostRecentAuthenticatedIdentity() } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift index 41fbc9a..6082eeb 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ContextService.swift @@ -14,12 +14,17 @@ public struct ContextService { private let mastodonAPIClient: MastodonAPIClient 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.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase sections = contentDatabase.contextPublisher(id: id) - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ConversationService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ConversationService.swift index e629e34..5c807fb 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ConversationService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ConversationService.swift @@ -12,9 +12,13 @@ public struct ConversationService { private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase - init(conversation: Conversation, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(conversation: Conversation, + environment: AppEnvironment, + mastodonAPIClient: MastodonAPIClient, + contentDatabase: ContentDatabase) { self.conversation = conversation self.navigationService = NavigationService( + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) self.mastodonAPIClient = mastodonAPIClient diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift index 35c0bd7..cfb4f3a 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ConversationsService.swift @@ -15,14 +15,17 @@ public struct ConversationsService { private let contentDatabase: ContentDatabase private let nextPageMaxIdSubject = PassthroughSubject() - init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase sections = contentDatabase.conversationsPublisher() .map { [.init(items: $0.map(CollectionItem.conversation))] } .eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService( + environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ExploreService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ExploreService.swift index e5963fd..38b44dc 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ExploreService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ExploreService.swift @@ -12,10 +12,12 @@ public struct ExploreService { private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase - init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index 9ab2908..4f66fed 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -39,6 +39,7 @@ public struct IdentityService { keychain: environment.keychain) navigationService = NavigationService( + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } @@ -98,6 +99,10 @@ public extension IdentityService { identityDatabase.recentIdentitiesPublisher(excluding: id) } + func otherAuthenticatedIdentitiesPublisher() -> AnyPublisher<[Identity], Error> { + identityDatabase.authenticatedIdentitiesPublisher(excluding: id) + } + func refreshLists() -> AnyPublisher { mastodonAPIClient.request(ListsEndpoint.lists) .flatMap(contentDatabase.setLists(_:)) @@ -249,6 +254,7 @@ public extension IdentityService { .map { _ in NotificationService( notification: notification, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } @@ -259,27 +265,31 @@ public extension IdentityService { func service(accountList: AccountsEndpoint, titleComponents: [String]? = nil) -> AccountListService { AccountListService( endpoint: accountList, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase, titleComponents: titleComponents) } func exploreService() -> ExploreService { - ExploreService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + ExploreService(environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } func searchService() -> SearchService { - SearchService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + SearchService(environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } func notificationsService(excludeTypes: Set) -> NotificationsService { NotificationsService(excludeTypes: excludeTypes, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } func conversationsService() -> ConversationsService { - ConversationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + ConversationsService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } func domainBlocksService() -> DomainBlocksService { diff --git a/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift b/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift index 4b5655c..edbc0ce 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/NavigationService.swift @@ -17,11 +17,16 @@ public enum Navigation { } public struct NavigationService { + private let environment: AppEnvironment private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase 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.contentDatabase = contentDatabase self.status = status @@ -35,6 +40,7 @@ public extension NavigationService { .collection( TimelineService( timeline: .tag(tag), + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase))) .eraseToAnyPublisher() @@ -52,26 +58,38 @@ public extension NavigationService { } 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 { - ProfileService(id: id, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + ProfileService(id: id, + environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } func profileService(account: Account, relationship: Relationship? = nil) -> ProfileService { ProfileService(account: account, relationship: relationship, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } 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 { - AccountService(account: account, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + AccountService(account: account, + environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } func loadMoreService(loadMore: LoadMore) -> LoadMoreService { @@ -81,6 +99,7 @@ public extension NavigationService { func notificationService(notification: MastodonNotification) -> NotificationService { NotificationService( notification: notification, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } @@ -88,12 +107,16 @@ public extension NavigationService { func conversationService(conversation: Conversation) -> ConversationService { ConversationService( conversation: conversation, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } 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( TimelineService( timeline: .tag(tag.name), + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)) } else if let account = results.accounts.first { diff --git a/ServiceLayer/Sources/ServiceLayer/Services/NotificationService.swift b/ServiceLayer/Sources/ServiceLayer/Services/NotificationService.swift index 7edc0db..d10b106 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/NotificationService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/NotificationService.swift @@ -12,9 +12,13 @@ public struct NotificationService { private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase - init(notification: MastodonNotification, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(notification: MastodonNotification, + environment: AppEnvironment, + mastodonAPIClient: MastodonAPIClient, + contentDatabase: ContentDatabase) { self.notification = notification self.navigationService = NavigationService( + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase, status: nil) diff --git a/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift b/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift index bc56caa..5e910f4 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/NotificationsService.swift @@ -18,6 +18,7 @@ public struct NotificationsService { private let nextPageMaxIdSubject: CurrentValueSubject init(excludeTypes: Set, + environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.excludeTypes = excludeTypes @@ -37,7 +38,9 @@ public struct NotificationsService { }) .eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) } } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift b/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift index e7bbe1a..6f6d896 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/ProfileService.swift @@ -10,25 +10,32 @@ public struct ProfileService { public let profilePublisher: AnyPublisher private let id: Account.Id + private let environment: AppEnvironment private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase init(account: Account, relationship: Relationship?, + environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.init( id: account.id, account: account, relationship: relationship, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } - init(id: Account.Id, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(id: Account.Id, + environment: AppEnvironment, + mastodonAPIClient: MastodonAPIClient, + contentDatabase: ContentDatabase) { self.init(id: id, account: nil, relationship: nil, + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } @@ -37,9 +44,11 @@ public struct ProfileService { id: Account.Id, account: Account?, relationship: Relationship?, + environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.id = id + self.environment = environment self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase @@ -60,6 +69,7 @@ public extension ProfileService { func timelineService(profileCollection: ProfileCollection) -> TimelineService { TimelineService( timeline: .profile(accountId: id, profileCollection: profileCollection), + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } diff --git a/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift b/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift index 0eae36f..f63c753 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/SearchService.swift @@ -16,11 +16,13 @@ public struct SearchService { private let nextPageMaxIdSubject = PassthroughSubject() private let resultsSubject = PassthroughSubject<(Results, Search), Error>() - init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(environment: AppEnvironment, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) sections = resultsSubject.scan((.empty, nil)) { let (results, search) = $1 diff --git a/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift b/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift index 280437c..f9d443c 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/StatusService.swift @@ -9,15 +9,21 @@ import MastodonAPI public struct StatusService { public let status: Status public let navigationService: NavigationService + private let environment: AppEnvironment private let mastodonAPIClient: MastodonAPIClient private let contentDatabase: ContentDatabase - init(status: Status, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(environment: AppEnvironment, + status: Status, + mastodonAPIClient: MastodonAPIClient, + contentDatabase: ContentDatabase) { self.status = status self.navigationService = NavigationService( + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase, status: status.displayStatus) + self.environment = environment self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase } @@ -32,20 +38,28 @@ public extension StatusService { contentDatabase.toggleShowAttachments(id: status.displayStatus.id) } - func toggleReblogged() -> AnyPublisher { - mastodonAPIClient.request(status.displayStatus.reblogged - ? StatusEndpoint.unreblog(id: status.displayStatus.id) - : StatusEndpoint.reblog(id: status.displayStatus.id)) - .flatMap(contentDatabase.insert(status:)) - .eraseToAnyPublisher() + func toggleReblogged(identityId: Identity.Id?) -> AnyPublisher { + 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.reblog(id: status.displayStatus.id)) + .flatMap(contentDatabase.insert(status:)) + .eraseToAnyPublisher() + } } - func toggleFavorited() -> AnyPublisher { - mastodonAPIClient.request(status.displayStatus.favourited - ? StatusEndpoint.unfavourite(id: status.displayStatus.id) - : StatusEndpoint.favourite(id: status.displayStatus.id)) - .flatMap(contentDatabase.insert(status:)) - .eraseToAnyPublisher() + func toggleFavorited(identityId: Identity.Id?) -> AnyPublisher { + 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.favourite(id: status.displayStatus.id)) + .flatMap(contentDatabase.insert(status:)) + .eraseToAnyPublisher() + } } func toggleBookmarked() -> AnyPublisher { @@ -84,7 +98,8 @@ public extension StatusService { if let inReplyToId = status.displayStatus.inReplyToId { inReplyToPublisher = mastodonAPIClient.request(StatusEndpoint.status(id: inReplyToId)) .map { - Self(status: $0, + Self(environment: environment, + status: $0, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) as Self? } @@ -103,6 +118,7 @@ public extension StatusService { func rebloggedByService() -> AccountListService { AccountListService( endpoint: .rebloggedBy(id: status.id), + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } @@ -110,6 +126,7 @@ public extension StatusService { func favoritedByService() -> AccountListService { AccountListService( endpoint: .favouritedBy(id: status.id), + environment: environment, mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) } @@ -130,3 +147,28 @@ public extension StatusService { .eraseToAnyPublisher() } } + +private extension StatusService { + func request(identityId: Identity.Id, + endpointClosure: @escaping (Status.Id) -> StatusEndpoint) -> AnyPublisher { + 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() + } +} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift index 1d92077..68b890e 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/TimelineService.swift @@ -21,12 +21,17 @@ public struct TimelineService { private let nextPageMaxIdSubject = PassthroughSubject() private let accountIdsForRelationshipsSubject = PassthroughSubject, Never>() - init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { + init(timeline: Timeline, + environment: AppEnvironment, + mastodonAPIClient: MastodonAPIClient, + contentDatabase: ContentDatabase) { self.timeline = timeline self.mastodonAPIClient = mastodonAPIClient self.contentDatabase = contentDatabase sections = contentDatabase.timelinePublisher(timeline) - navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) + navigationService = NavigationService(environment: environment, + mastodonAPIClient: mastodonAPIClient, + contentDatabase: contentDatabase) nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher() accountIdsForRelationships = accountIdsForRelationshipsSubject.eraseToAnyPublisher() diff --git a/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift b/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift index 3c3bd05..8859fb1 100644 --- a/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift +++ b/ViewModels/Sources/PreviewViewModels/PreviewViewModels.swift @@ -81,6 +81,7 @@ public extension ReportViewModel { static let preview = ReportViewModel( accountService: AccountService( account: .preview, + environment: environment, mastodonAPIClient: .preview, contentDatabase: .preview), identityContext: .preview) @@ -90,6 +91,7 @@ public extension MuteViewModel { static let preview = MuteViewModel( accountService: AccountService( account: .preview, + environment: environment, mastodonAPIClient: .preview, contentDatabase: .preview), identityContext: .preview) diff --git a/ViewModels/Sources/ViewModels/Entities/IdentityContext.swift b/ViewModels/Sources/ViewModels/Entities/IdentityContext.swift index 609e3f2..67cde37 100644 --- a/ViewModels/Sources/ViewModels/Entities/IdentityContext.swift +++ b/ViewModels/Sources/ViewModels/Entities/IdentityContext.swift @@ -6,6 +6,7 @@ import ServiceLayer public final class IdentityContext: ObservableObject { @Published private(set) public var identity: Identity + @Published private(set) public var authenticatedOtherIdentities = [Identity]() @Published public var appPreferences: AppPreferences let service: IdentityService @@ -19,6 +20,9 @@ public final class IdentityContext: ObservableObject { DispatchQueue.main.async { publisher.dropFirst().assign(to: &self.$identity) + service.otherAuthenticatedIdentitiesPublisher() + .replaceError(with: []) + .assign(to: &self.$authenticatedOtherIdentities) } } } diff --git a/ViewModels/Sources/ViewModels/View Models/NewStatusViewModel.swift b/ViewModels/Sources/ViewModels/View Models/NewStatusViewModel.swift index 3a7cc24..66e0a50 100644 --- a/ViewModels/Sources/ViewModels/View Models/NewStatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/NewStatusViewModel.swift @@ -9,7 +9,6 @@ public final class NewStatusViewModel: ObservableObject { @Published public var visibility: Status.Visibility @Published public private(set) var compositionViewModels = [CompositionViewModel]() @Published public private(set) var identityContext: IdentityContext - @Published public private(set) var authenticatedIdentities = [Identity]() @Published public var canPost = false @Published public var alertItem: AlertItem? @Published public private(set) var postingState = PostingState.composing @@ -87,14 +86,6 @@ public final class NewStatusViewModel: ObservableObject { } 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)) } .receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring .compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) } diff --git a/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift b/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift index 6a233e0..162c7c9 100644 --- a/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/StatusViewModel.swift @@ -255,16 +255,16 @@ public extension StatusViewModel { .eraseToAnyPublisher()) } - func toggleReblogged() { + func toggleReblogged(identityId: Identity.Id? = nil) { eventsSubject.send( - statusService.toggleReblogged() + statusService.toggleReblogged(identityId: identityId) .map { _ in .ignorableOutput } .eraseToAnyPublisher()) } - func toggleFavorited() { + func toggleFavorited(identityId: Identity.Id? = nil) { eventsSubject.send( - statusService.toggleFavorited() + statusService.toggleFavorited(identityId: identityId) .map { _ in .ignorableOutput } .eraseToAnyPublisher()) } diff --git a/Views/UIKit/CompositionView.swift b/Views/UIKit/CompositionView.swift index 9ea40b7..c09b4f4 100644 --- a/Views/UIKit/CompositionView.swift +++ b/Views/UIKit/CompositionView.swift @@ -74,7 +74,8 @@ private extension CompositionView { avatarImageView.isUserInteractionEnabled = true changeIdentityButton.setBackgroundImage(.highlightedButtonBackground, for: .highlighted) changeIdentityButton.showsMenuAsPrimaryAction = true - changeIdentityButton.menu = changeIdentityMenu(identities: parentViewModel.authenticatedIdentities) + changeIdentityButton.menu = + changeIdentityMenu(identities: parentViewModel.identityContext.authenticatedOtherIdentities) let stackView = UIStackView() @@ -205,7 +206,7 @@ private extension CompositionView { } .store(in: &cancellables) - parentViewModel.$authenticatedIdentities + parentViewModel.identityContext.$authenticatedOtherIdentities .sink { [weak self] in self?.changeIdentityButton.menu = self?.changeIdentityMenu(identities: $0) } .store(in: &cancellables) diff --git a/Views/UIKit/Content Views/StatusView.swift b/Views/UIKit/Content Views/StatusView.swift index 90c9d3b..036c685 100644 --- a/Views/UIKit/Content Views/StatusView.swift +++ b/Views/UIKit/Content Views/StatusView.swift @@ -616,9 +616,11 @@ private extension StatusView { setReblogButtonColor(reblogged: viewModel.reblogged) reblogButton.isEnabled = viewModel.canBeReblogged && isAuthenticated + reblogButton.menu = authenticatedIdentitiesMenu { viewModel.toggleReblogged(identityId: $0.id) } setFavoriteButtonColor(favorited: viewModel.favorited) favoriteButton.isEnabled = isAuthenticated + favoriteButton.menu = authenticatedIdentitiesMenu { viewModel.toggleFavorited(identityId: $0.id) } shareButton.tag = viewModel.sharingURL?.hashValue ?? 0 @@ -1127,6 +1129,38 @@ private extension StatusView { 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 {