diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift index a138f066d..56742f827 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift @@ -259,9 +259,15 @@ enum RelationshipElement: Equatable { case acceptRejectButtons(isFollowing: Bool) case acceptedLabel case rejectedLabel - case mutualLabel - case followingLabel - case pendingRequestLabel + case mutualButton + case followingButton + case pendingRequestButton + + enum FollowAction { + case follow + case unfollow + case noAction + } var description: String { switch self { @@ -283,11 +289,11 @@ enum RelationshipElement: Equatable { return "accepted" case .rejectedLabel: return "rejected" - case .mutualLabel: + case .mutualButton: return "mutual" - case .followingLabel: + case .followingButton: return "following" - case .pendingRequestLabel: + case .pendingRequestButton: return "pending" } } @@ -296,7 +302,34 @@ enum RelationshipElement: Equatable { { return lhs.description == rhs.description } + + var followAction: FollowAction { + switch self { + case .followButton, .requestButton: + return .follow + case .followingButton, .mutualButton, .pendingRequestButton: + return .unfollow + default: + return .noAction + } + } + var buttonText: String? { + switch self { + case .followButton: + return L10n.Common.Controls.Friendship.follow + case .requestButton: + return L10n.Common.Controls.Friendship.request + case .mutualButton: + return L10n.Common.Controls.Friendship.mutual + case .followingButton: + return L10n.Common.Controls.Friendship.following + case .pendingRequestButton: + return L10n.Common.Controls.Friendship.pending + default: + return nil + } + } } extension Mastodon.Entity.Relationship { @@ -304,9 +337,9 @@ extension Mastodon.Entity.Relationship { var relationshipElement: RelationshipElement? { switch (following, followedBy) { case (true, true): - return .mutualLabel + return .mutualButton case (true, false): - return .followingLabel + return .followingButton case (false, true): if let account: NotificationAuthor = MastodonFeedItemCacheManager .shared.fullAccount(id) @@ -314,7 +347,7 @@ extension Mastodon.Entity.Relationship { account.locked { if requested { - return .pendingRequestLabel + return .pendingRequestButton } else { return .requestButton } @@ -549,7 +582,7 @@ struct NotificationRowView: View { trailingElement, grouped: accountInfo.totalActorCount > 1) } } - .frame(height: smallAvatarSize) // this keeps GeometryReader from causing inconsistent visual spacing in the VStack + .frame(height: smallAvatarSize) // this keeps GeometryReader from causing inconsistent visual spacing in the VStack } @ViewBuilder @@ -559,21 +592,14 @@ struct NotificationRowView: View { switch (elementType, grouped) { case (.fetching, false): ProgressView().progressViewStyle(.circular) - case (.followButton, false): - Button(L10n.Common.Controls.Friendship.follow) { - viewModel.doAvatarRowButtonAction() + case (.followButton, false), (.mutualButton, false), + (.followingButton, false), (.pendingRequestButton, false): + if let buttonText = elementType.buttonText { + Button(buttonText) { + viewModel.doAvatarRowButtonAction() + } + .buttonStyle(FollowButton(elementType)) } - .buttonStyle(.borderedProminent) - .foregroundStyle(.background) - .controlSize(.small) - .bold() - case (.requestButton, false): - Button(L10n.Common.Controls.Friendship.request) { - viewModel.doAvatarRowButtonAction() - } - .buttonStyle(.borderedProminent) - .controlSize(.small) - .bold() case (.acceptRejectButtons(let isFollowing), false): HStack { @@ -599,12 +625,6 @@ struct NotificationRowView: View { Text(L10n.Scene.Notification.FollowRequest.accepted) case (.rejectedLabel, false): Text(L10n.Scene.Notification.FollowRequest.rejected) - case (.mutualLabel, false): - Text(L10n.Common.Controls.Friendship.mutual) - case (.followingLabel, false): - Text(L10n.Common.Controls.Friendship.following) - case (.pendingRequestLabel, false): - Text(L10n.Common.Controls.Friendship.pending) case (.error(_), _): Image(systemName: "exclamationmark.triangle").foregroundStyle(.gray) default: @@ -852,3 +872,58 @@ extension Mastodon.Entity.Status { navigateToStatus: navigateToStatus) } } + +struct FollowButton: ButtonStyle { + private let followAction: RelationshipElement.FollowAction + + init(_ relationshipElement: RelationshipElement) { + followAction = relationshipElement.followAction + } + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding([.horizontal], 12) + .padding([.vertical], 8) + .background(backgroundColor) + .foregroundStyle(textColor) + .controlSize(.small) + .fontWeight(fontWeight) + .clipShape(Capsule()) + } + + private var backgroundColor: Color { + switch followAction { + case .follow: + return Color(uiColor: Asset.Colors.Button.userFollow.color) + case .unfollow: + return Color(uiColor: Asset.Colors.Button.userFollowing.color) + case .noAction: + assertionFailure() + return .clear + } + } + + private var textColor: Color { + switch followAction { + case .follow: + return .white + case .unfollow: + return Color(uiColor: Asset.Colors.Button.userFollowingTitle.color) + case .noAction: + assertionFailure() + return .clear + } + } + + private var fontWeight: SwiftUICore.Font.Weight { + switch followAction { + case .follow: + return .bold + case .unfollow: + return .light + case .noAction: + assertionFailure() + return .regular + } + } +} diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift index 3fea7b09b..008897f70 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift @@ -238,7 +238,7 @@ class NotificationRowViewModel: ObservableObject { switch (type, relationship.following) { case (.follow, true): - element = .mutualLabel + element = .mutualButton case (.follow, false): element = .followButton case (.followRequest, _): @@ -312,8 +312,8 @@ extension NotificationRowViewModel { switch avatarRow { case .avatarRow(let accountInfo, let relationshipElement): switch relationshipElement { - case .followButton, .requestButton: - await doFollow(accountInfo) + case .followButton, .requestButton, .mutualButton, .followingButton, .pendingRequestButton: + await doFollowAction(relationshipElement.followAction, notificationSourceAccounts: accountInfo) case .acceptRejectButtons: await doAcceptFollowRequest(accountInfo, accept: accept) default: @@ -326,25 +326,33 @@ extension NotificationRowViewModel { } @MainActor - private func doFollow(_ accountInfo: NotificationSourceAccounts) async { - guard let accountID = accountInfo.firstAccountID, + private func doFollowAction(_ action: RelationshipElement.FollowAction, notificationSourceAccounts: NotificationSourceAccounts) async { + guard let accountID = notificationSourceAccounts.firstAccountID, let authBox = AuthenticationServiceProvider.shared.currentActiveUser .value else { return } let startingAvatarRow = avatarRow - avatarRow = .avatarRow(accountInfo, .fetching) + avatarRow = .avatarRow(notificationSourceAccounts, .fetching) do { let updatedElement: RelationshipElement - let response = try await APIService.shared.follow( - accountID, authenticationBox: authBox) - if response.following { - updatedElement = .followingLabel - } else if response.requested { - updatedElement = .pendingRequestLabel - } else { - updatedElement = .error(nil) + let response: Mastodon.Entity.Relationship + switch action { + case .follow: + response = try await APIService.shared.follow( + accountID, authenticationBox: authBox) + case .unfollow: + response = try await APIService.shared.unfollow(accountID, authenticationBox: authBox) + case .noAction: + throw AppError.unexpected("action attempted for relationship element that has no action") } - avatarRow = .avatarRow(accountInfo, updatedElement) + if response.following { + updatedElement = .followingButton + } else if response.requested { + updatedElement = .pendingRequestButton + } else { + updatedElement = .followButton + } + avatarRow = .avatarRow(notificationSourceAccounts, updatedElement) } catch { presentError(error) avatarRow = startingAvatarRow diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift index 2bf6bd1eb..67770ac68 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Follow.swift @@ -70,6 +70,15 @@ extension APIService { authorization: authenticationBox.userAuthorization ).singleOutput().value } + + public func unfollow(_ accountID: String, authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Entity.Relationship { + return try await Mastodon.API.Account.unfollow( + session: session, + domain: authenticationBox.domain, + accountID: accountID, + authorization: authenticationBox.userAuthorization + ).singleOutput().value + } public func toggleShowReblogs( for user: Mastodon.Entity.Account,