diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index f8bec6e..77973e4 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; }; F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; }; F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; }; + F8210DEC2966F30C001D9973 /* UserFeedbackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DEB2966F30C001D9973 /* UserFeedbackService.swift */; }; F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */; }; F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; }; F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; }; @@ -83,6 +84,7 @@ F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = ""; }; F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = ""; }; F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = ""; }; + F8210DEB2966F30C001D9973 /* UserFeedbackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFeedbackService.swift; sourceTree = ""; }; F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Exif.swift"; sourceTree = ""; }; F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = ""; }; F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = ""; }; @@ -290,6 +292,7 @@ F85D4974296407F100751DF7 /* TimelineService.swift */, F8A93D7F2965FED4001D8331 /* AccountService.swift */, F8210DE02966D0C4001D9973 /* StatusService.swift */, + F8210DEB2966F30C001D9973 /* UserFeedbackService.swift */, ); path = Services; sourceTree = ""; @@ -427,6 +430,7 @@ F8A93D802965FED4001D8331 /* AccountService.swift in Sources */, F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */, F85D4973296406E700751DF7 /* BottomRight.swift in Sources */, + F8210DEC2966F30C001D9973 /* UserFeedbackService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift b/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift index f43adfb..3319883 100644 --- a/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift +++ b/Vernissage/Extensions/MastodonClientAuthenticated+Account.swift @@ -41,4 +41,15 @@ extension MastodonClientAuthenticated { let (data, _) = try await urlSession.data(for: request) return try JSONDecoder().decode([Status].self, from: data) } + + func follow(for accountId: String) async throws -> Relationship { + let request = try Self.request( + for: baseURL, + target: Mastodon.Account.follow(accountId), + withBearerToken: token + ) + + let (data, _) = try await urlSession.data(for: request) + return try JSONDecoder().decode(Relationship.self, from: data) + } } diff --git a/Vernissage/Services/AccountService.swift b/Vernissage/Services/AccountService.swift index 5fb87a6..9487692 100644 --- a/Vernissage/Services/AccountService.swift +++ b/Vernissage/Services/AccountService.swift @@ -37,4 +37,13 @@ public class AccountService { let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) return try await client.getStatuses(for: accountId) } + + public func follow(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.follow(for: accountId) + } } diff --git a/Vernissage/Services/StatusService.swift b/Vernissage/Services/StatusService.swift index 02da639..77a314f 100644 --- a/Vernissage/Services/StatusService.swift +++ b/Vernissage/Services/StatusService.swift @@ -11,7 +11,57 @@ public class StatusService { public static let shared = StatusService() private init() { } - func copy(from status: Status, to statusData: StatusData) { + func favourite(statusId: String, accountData: AccountData?) async throws -> Status? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.favourite(statusId: statusId) + } + + func unfavourite(statusId: String, accountData: AccountData?) async throws -> Status? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.unfavourite(statusId: statusId) + } + + func boost(statusId: String, accountData: AccountData?) async throws -> Status? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.boost(statusId: statusId) + } + + func unboost(statusId: String, accountData: AccountData?) async throws -> Status? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.unboost(statusId: statusId) + } + + func bookmark(statusId: String, accountData: AccountData?) async throws -> Status? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.bookmark(statusId: statusId) + } + + func unbookmark(statusId: String, accountData: AccountData?) async throws -> Status? { + guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else { + return nil + } + + let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken) + return try await client.unbookmark(statusId: statusId) } } diff --git a/Vernissage/Services/UserFeedbackService.swift b/Vernissage/Services/UserFeedbackService.swift new file mode 100644 index 0000000..f0bf352 --- /dev/null +++ b/Vernissage/Services/UserFeedbackService.swift @@ -0,0 +1,18 @@ +// +// https://mczachurski.dev +// Copyright © 2023 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import AVFoundation + +public class UserFeedbackService { + public static let shared = UserFeedbackService() + private init() { } + + func send() { + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + // AudioServicesPlaySystemSound(1016) + } +} diff --git a/Vernissage/Views/UserProfileView.swift b/Vernissage/Views/UserProfileView.swift index 592da61..3a40800 100644 --- a/Vernissage/Views/UserProfileView.swift +++ b/Vernissage/Views/UserProfileView.swift @@ -79,7 +79,19 @@ struct UserProfileView: View { Spacer() Button { - // TODO: Folllow/Unfollow. + Task { + do { + if let relationship = try await AccountService.shared.follow( + forAccountId: self.accountId, + andContext: self.applicationState.accountData + ) { + UserFeedbackService.shared.send() + self.relationship = relationship + } + } catch { + print("Error \(error.localizedDescription)") + } + } } label: { HStack { Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus") diff --git a/Vernissage/Widgets/InteractionRow.swift b/Vernissage/Widgets/InteractionRow.swift index 3da7619..39d155c 100644 --- a/Vernissage/Widgets/InteractionRow.swift +++ b/Vernissage/Widgets/InteractionRow.swift @@ -7,12 +7,14 @@ import SwiftUI struct InteractionRow: View { + @EnvironmentObject var applicationState: ApplicationState @ObservedObject public var statusData: StatusData var body: some View { HStack (alignment: .top) { Button { // TODO: Reply. + UserFeedbackService.shared.send() } label: { HStack(alignment: .center) { Image(systemName: "message") @@ -24,7 +26,24 @@ struct InteractionRow: View { Spacer() Button { - // TODO: Reboost. + Task { + do { + let status = self.statusData.reblogged + ? try await StatusService.shared.unboost(statusId: self.statusData.id, accountData: self.applicationState.accountData) + : try await StatusService.shared.boost(statusId: self.statusData.id, accountData: self.applicationState.accountData) + + if let status { + self.statusData.reblogsCount = status.reblogsCount == self.statusData.reblogsCount + ? Int32(status.reblogsCount + 1) + : Int32(status.reblogsCount) + + self.statusData.reblogged = status.reblogged + UserFeedbackService.shared.send() + } + } catch { + print("Error \(error.localizedDescription)") + } + } } label: { HStack(alignment: .center) { Image(systemName: statusData.reblogged ? "paperplane.fill" : "paperplane") @@ -36,7 +55,24 @@ struct InteractionRow: View { Spacer() Button { - // TODO: Favorite. + Task { + do { + let status = self.statusData.favourited + ? try await StatusService.shared.unfavourite(statusId: self.statusData.id, accountData: self.applicationState.accountData) + : try await StatusService.shared.favourite(statusId: self.statusData.id, accountData: self.applicationState.accountData) + + if let status { + self.statusData.favouritesCount = status.favouritesCount == self.statusData.favouritesCount + ? Int32(status.favouritesCount + 1) + : Int32(status.favouritesCount) + + self.statusData.favourited = status.favourited + UserFeedbackService.shared.send() + } + } catch { + print("Error \(error.localizedDescription)") + } + } } label: { HStack(alignment: .center) { Image(systemName: statusData.favourited ? "hand.thumbsup.fill" : "hand.thumbsup") @@ -48,7 +84,18 @@ struct InteractionRow: View { Spacer() Button { - // TODO: Bookmark. + Task { + do { + let status = self.statusData.bookmarked + ? try await StatusService.shared.unbookmark(statusId: self.statusData.id, accountData: self.applicationState.accountData) + : try await StatusService.shared.bookmark(statusId: self.statusData.id, accountData: self.applicationState.accountData) + + self.statusData.bookmarked.toggle() + UserFeedbackService.shared.send() + } catch { + print("Error \(error.localizedDescription)") + } + } } label: { Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark") } @@ -57,6 +104,7 @@ struct InteractionRow: View { Button { // TODO: Share. + UserFeedbackService.shared.send() } label: { Image(systemName: "square.and.arrow.up") }