diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 131822d2..31ec3dbf 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -382,8 +382,9 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 200; + CURRENT_PROJECT_VERSION = 250; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_TEAM = Z6P74P6T99; @@ -405,7 +406,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.0.3; + MARKETING_VERSION = 0.2; PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -425,8 +426,9 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = IceCubesApp/IceCubesApp.entitlements; CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 200; + CURRENT_PROJECT_VERSION = 250; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"IceCubesApp/Resources\""; DEVELOPMENT_TEAM = Z6P74P6T99; @@ -448,7 +450,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.0.3; + MARKETING_VERSION = 0.2; PRODUCT_BUNDLE_IDENTIFIER = com.thomasricouard.IceCubesApp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index 58f7c7d6..7fdfea7e 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -7,7 +7,10 @@ struct AccountDetailHeaderView: View { @EnvironmentObject private var routeurPath: RouterPath @Environment(\.redactionReasons) private var reasons + let isCurrentUser: Bool let account: Account + @Binding var relationship: Relationshionship? + @Binding var following: Bool var body: some View { VStack(alignment: .leading) { @@ -18,20 +21,31 @@ struct AccountDetailHeaderView: View { private var headerImageView: some View { GeometryReader { proxy in - AsyncImage( - url: account.header, - content: { image in - image.resizable() - .aspectRatio(contentMode: .fill) - .frame(height: 200) - .frame(width: proxy.frame(in: .local).width) - .clipped() - }, - placeholder: { - Color.gray - .frame(height: 200) + ZStack(alignment: .bottomTrailing) { + AsyncImage( + url: account.header, + content: { image in + image.resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 200) + .frame(width: proxy.frame(in: .local).width) + .clipped() + }, + placeholder: { + Color.gray + .frame(height: 200) + } + ) + if relationship?.followedBy == true { + Text("Follows You") + .font(.footnote) + .fontWeight(.semibold) + .padding(4) + .background(.ultraThinMaterial) + .cornerRadius(4) + .padding(8) } - ) + } .background(Color.gray) } .frame(height: 200) @@ -76,11 +90,27 @@ struct AccountDetailHeaderView: View { private var accountInfoView: some View { Group { accountAvatarView - Text(account.displayName) - .font(.headline) - Text(account.acct) - .font(.callout) - .foregroundColor(.gray) + HStack { + VStack(alignment: .leading, spacing: 0) { + Text(account.displayName) + .font(.headline) + Text(account.acct) + .font(.callout) + .foregroundColor(.gray) + } + Spacer() + if relationship != nil && !isCurrentUser { + Button { + following.toggle() + } label: { + if relationship?.requested == true { + Text("Requested") + } else { + Text(following ? "Following" : "Follow") + } + }.buttonStyle(.bordered) + } + } Text(account.note.asSafeAttributedString) .font(.body) .padding(.top, 8) @@ -102,6 +132,9 @@ struct AccountDetailHeaderView: View { struct AccountDetailHeaderView_Previews: PreviewProvider { static var previews: some View { - AccountDetailHeaderView(account: .placeholder()) + AccountDetailHeaderView(isCurrentUser: false, + account: .placeholder(), + relationship: .constant(.placeholder()), + following: .constant(true)) } } diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 59e7cb72..f0da322c 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -18,7 +18,8 @@ public struct AccountDetailView: View { } public init(account: Account, isCurrentUser: Bool = false) { - _viewModel = StateObject(wrappedValue: .init(account: account)) + _viewModel = StateObject(wrappedValue: .init(account: account, + isCurrentUser: isCurrentUser)) self.isCurrentUser = isCurrentUser } @@ -54,14 +55,30 @@ public struct AccountDetailView: View { private var headerView: some View { switch viewModel.state { case .loading: - AccountDetailHeaderView(account: .placeholder()) + AccountDetailHeaderView(isCurrentUser: isCurrentUser, + account: .placeholder(), + relationship: .constant(.placeholder()), + following: .constant(false)) .redacted(reason: .placeholder) case let .data(account): - AccountDetailHeaderView(account: account) + AccountDetailHeaderView(isCurrentUser: isCurrentUser, + account: account, + relationship: $viewModel.relationship, + following: + .init(get: { + viewModel.relationship?.following ?? false + }, set: { following in + Task { + if following { + await viewModel.follow() + } else { + await viewModel.unfollow() + } + } + })) case let .error(error): Text("Error: \(error.localizedDescription)") } - } } diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index becf4cf0..8e6ddebd 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -16,23 +16,32 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { @Published var state: State = .loading @Published var statusesState: StatusesState = .loading @Published var title: String = "" + @Published var relationship: Relationshionship? private var account: Account? + private(set) var statuses: [Status] = [] + private let isCurrentUser: Bool init(accountId: String) { self.accountId = accountId + self.isCurrentUser = false } - init(account: Account) { + init(account: Account, isCurrentUser: Bool) { self.accountId = account.id self.state = .data(account: account) + self.isCurrentUser = isCurrentUser } func fetchAccount() async { guard let client else { return } do { let account: Account = try await client.get(endpoint: Accounts.accounts(id: accountId)) + if !isCurrentUser { + let relationships: [Relationshionship] = try await client.get(endpoint: Accounts.relationships(id: accountId)) + self.relationship = relationships.first + } self.title = account.displayName state = .data(account: account) } catch { @@ -63,4 +72,22 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { statusesState = .error(error: error) } } + + func follow() async { + guard let client else { return } + do { + relationship = try await client.post(endpoint: Accounts.follow(id: accountId)) + } catch { + print("Error while following: \(error.localizedDescription)") + } + } + + func unfollow() async { + guard let client else { return } + do { + relationship = try await client.post(endpoint: Accounts.unfollow(id: accountId)) + } catch { + print("Error while unfollowing: \(error.localizedDescription)") + } + } } diff --git a/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.xcassets/brandDisabled.colorset/Contents.json b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.xcassets/brandDisabled.colorset/Contents.json new file mode 100644 index 00000000..6c46e5d4 --- /dev/null +++ b/Packages/DesignSystem/Sources/DesignSystem/Resources/Colors.xcassets/brandDisabled.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.353", + "red" : "0.349" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Packages/Models/Sources/Models/Relationshionship.swift b/Packages/Models/Sources/Models/Relationshionship.swift new file mode 100644 index 00000000..6e2dee38 --- /dev/null +++ b/Packages/Models/Sources/Models/Relationshionship.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct Relationshionship: Codable { + public let id: String + public let following: Bool + public let showingReblogs: Bool + public let followedBy: Bool + public let blocking: Bool + public let blockedBy: Bool + public let muting: Bool + public let mutingNotifications: Bool + public let requested: Bool + public let domainBlocking: Bool + public let endorsed: Bool + public let note: String + + static public func placeholder() -> Relationshionship { + .init(id: UUID().uuidString, + following: false, + showingReblogs: false, + followedBy: false, + blocking: false, + blockedBy: false, + muting: false, + mutingNotifications: false, + requested: false, + domainBlocking: false, + endorsed: false, + note: "") + } +} diff --git a/Packages/Network/Sources/Network/Endpoint/Accounts.swift b/Packages/Network/Sources/Network/Endpoint/Accounts.swift index f48462af..2e579a26 100644 --- a/Packages/Network/Sources/Network/Endpoint/Accounts.swift +++ b/Packages/Network/Sources/Network/Endpoint/Accounts.swift @@ -4,6 +4,9 @@ public enum Accounts: Endpoint { case accounts(id: String) case verifyCredentials case statuses(id: String, sinceId: String?) + case relationships(id: String) + case follow(id: String) + case unfollow(id: String) public func path() -> String { switch self { @@ -13,6 +16,12 @@ public enum Accounts: Endpoint { return "accounts/verify_credentials" case .statuses(let id, _): return "accounts/\(id)/statuses" + case .relationships: + return "accounts/relationships" + case .follow(let id): + return "accounts/\(id)/follow" + case .unfollow(let id): + return "accounts/\(id)/unfollow" } } @@ -21,6 +30,8 @@ public enum Accounts: Endpoint { case .statuses(_, let sinceId): guard let sinceId else { return nil } return [.init(name: "max_id", value: sinceId)] + case let .relationships(id): + return [.init(name: "id", value: id)] default: return nil }