diff --git a/MastodonKit/Sources/MastodonKit/Errors/NetworkError.swift b/MastodonKit/Sources/MastodonKit/Errors/NetworkError.swift index 21911f9..c6b3d38 100644 --- a/MastodonKit/Sources/MastodonKit/Errors/NetworkError.swift +++ b/MastodonKit/Sources/MastodonKit/Errors/NetworkError.swift @@ -15,7 +15,7 @@ extension NetworkError: LocalizedError { switch self { case .notSuccessResponse(let response): let statusCode = response.statusCode() - return NSLocalizedString("Network request returned not success status code: '\(statusCode?.localizedDescription ?? "unknown")'.", comment: "It's error returned from remote server.") + return NSLocalizedString("Network request returned not success status code: '\(statusCode?.localizedDescription ?? "unknown")'. Request URL: '\(response.url?.string ?? "unknown")'.", comment: "It's error returned from remote server.") } } } diff --git a/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift b/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift index 14f08e1..b66f5b3 100644 --- a/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift +++ b/MastodonKit/Sources/MastodonKit/MastodonClient+Account.swift @@ -74,6 +74,46 @@ public extension MastodonClientAuthenticated { return try await downloadJson(Relationship.self, request: request) } + func mute(for accountId: String) async throws -> Relationship { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.mute(accountId), + withBearerToken: token + ) + + return try await downloadJson(Relationship.self, request: request) + } + + func unmute(for accountId: String) async throws -> Relationship { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.unmute(accountId), + withBearerToken: token + ) + + return try await downloadJson(Relationship.self, request: request) + } + + func block(for accountId: String) async throws -> Relationship { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.block(accountId), + withBearerToken: token + ) + + return try await downloadJson(Relationship.self, request: request) + } + + func unblock(for accountId: String) async throws -> Relationship { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.unblock(accountId), + withBearerToken: token + ) + + return try await downloadJson(Relationship.self, request: request) + } + func getFollowers(for accountId: String, page: Int = 1) async throws -> [Account] { let request = try Self.request( for: baseURL, diff --git a/Vernissage/Services/AccountService.swift b/Vernissage/Services/AccountService.swift index bb11daf..1001468 100644 --- a/Vernissage/Services/AccountService.swift +++ b/Vernissage/Services/AccountService.swift @@ -69,6 +69,42 @@ public class AccountService { return try await client.unfollow(for: accountId) } + public func mute(forAccountId accountId: String, andContext accountData: AccountData?) async throws -> Relationship? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.mute(for: accountId) + } + + public func unmute(forAccountId accountId: String, andContext accountData: AccountData?) async throws -> Relationship? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.unmute(for: accountId) + } + + public func block(forAccountId accountId: String, andContext accountData: AccountData?) async throws -> Relationship? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.block(for: accountId) + } + + public func unblock(forAccountId accountId: String, andContext accountData: AccountData?) async throws -> Relationship? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.unblock(for: accountId) + } + public func getFollowers(forAccountId accountId: String, andContext accountData: AccountData?, page: Int) async throws -> [Account] { guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { return [] diff --git a/Vernissage/Widgets/InteractionRow.swift b/Vernissage/Widgets/InteractionRow.swift index c2f9002..1c7ac72 100644 --- a/Vernissage/Widgets/InteractionRow.swift +++ b/Vernissage/Widgets/InteractionRow.swift @@ -108,13 +108,11 @@ struct InteractionRow: View { Image(systemName: self.bookmarked ? "bookmark.fill" : "bookmark") } - Spacer() - - ActionButton { - // TODO: Share. - ToastrService.shared.showError(subtitle: "Sending new status failed!") - } label: { - Image(systemName: "square.and.arrow.up") + if let url = statusViewModel.url { + Spacer() + ShareLink(item: url) { + Image(systemName: "square.and.arrow.up") + } } } .font(.title3) @@ -133,11 +131,3 @@ struct InteractionRow: View { self.bookmarked = self.statusViewModel.bookmarked } } - -struct InteractionRow_Previews: PreviewProvider { - static var previews: some View { - Text("") -// InteractionRow(status: Status(id: "", content: "", application: Application(name: ""))) -// .previewLayout(.fixed(width: 300, height: 70)) - } -} diff --git a/Vernissage/Widgets/UserProfile/UserProfileHeader.swift b/Vernissage/Widgets/UserProfile/UserProfileHeader.swift index 1fd0a3c..43e9cad 100644 --- a/Vernissage/Widgets/UserProfile/UserProfileHeader.swift +++ b/Vernissage/Widgets/UserProfile/UserProfileHeader.swift @@ -69,18 +69,7 @@ struct UserProfileHeader: View { Spacer() - if self.applicationState.accountData?.id != self.account.id { - ActionButton { - await onRelationshipButtonTap() - } label: { - HStack { - Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus") - Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow")) - } - } - .buttonStyle(.borderedProminent) - .tint(relationship?.following == true ? .dangerColor : .accentColor) - } + self.actionButtons() } if let note = account.note, !note.isEmpty { @@ -97,6 +86,65 @@ struct UserProfileHeader: View { .padding() } + @ViewBuilder + private func actionButtons() -> some View { + if self.applicationState.accountData?.id != self.account.id { + ActionButton { + await onRelationshipButtonTap() + } label: { + HStack { + Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus") + Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow")) + } + } + .buttonStyle(.borderedProminent) + .tint(relationship?.following == true ? .dangerColor : .accentColor) + + Menu (content: { + if let accountUrl = account.url { + Link(destination: accountUrl) { + Label("Open link to profile", systemImage: "safari") + } + + ShareLink(item: accountUrl) { + Label("Share", systemImage: "square.and.arrow.up") + } + + Divider() + } + + Button { + Task { + await onMuteAccount() + } + } label: { + if self.relationship?.muting == true { + Label("Unute", systemImage: "message.and.waveform.fill") + } else { + Label("Mute", systemImage: "message.and.waveform") + } + } + + Button { + Task { + await onBlockAccount() + } + } label: { + if self.relationship?.blocking == true { + Label("Unblock", systemImage: "hand.raised.fill") + } else { + Label("Block", systemImage: "hand.raised") + } + } + + }, label: { + Image(systemName: "ellipsis.circle") + }) + .buttonStyle(.borderedProminent) + .tint(Color.secondaryLabel) + } + } + private func onRelationshipButtonTap() async { do { if self.relationship?.following == true { @@ -118,5 +166,49 @@ struct UserProfileHeader: View { ErrorService.shared.handle(error, message: "Relationship action failed.", showToastr: true) } } + + private func onMuteAccount() async { + do { + if self.relationship?.muting == true { + if let relationship = try await AccountService.shared.unmute( + forAccountId: self.account.id, + andContext: self.applicationState.accountData + ) { + self.relationship = relationship + } + } else { + if let relationship = try await AccountService.shared.mute( + forAccountId: self.account.id, + andContext: self.applicationState.accountData + ) { + self.relationship = relationship + } + } + } catch { + ErrorService.shared.handle(error, message: "Muting/unmuting action failed.", showToastr: true) + } + } + + private func onBlockAccount() async { + do { + if self.relationship?.blocking == true { + if let relationship = try await AccountService.shared.unblock( + forAccountId: self.account.id, + andContext: self.applicationState.accountData + ) { + self.relationship = relationship + } + } else { + if let relationship = try await AccountService.shared.block( + forAccountId: self.account.id, + andContext: self.applicationState.accountData + ) { + self.relationship = relationship + } + } + } catch { + ErrorService.shared.handle(error, message: "Block/unblock action failed.", showToastr: true) + } + } }