Follow / Unfollow

This commit is contained in:
Thomas Ricouard 2022-12-20 17:11:12 +01:00
parent 8def548913
commit 60a963441c
7 changed files with 169 additions and 28 deletions

View file

@ -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;

View file

@ -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))
}
}

View file

@ -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)")
}
}
}

View file

@ -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)")
}
}
}

View file

@ -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
}
}

View file

@ -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: "")
}
}

View file

@ -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
}