From d4110359307a8028fa2fa1485d99790e0d76ee5f Mon Sep 17 00:00:00 2001 From: shannon Date: Thu, 13 Feb 2025 09:18:41 -0500 Subject: [PATCH] Use images instead of text for buttons to approve/reject follow requests Includes refactoring of the RelationshipElement enum to make them less confusing. Contributes to #399 [BUG] Multiple interactions do not collapse into a single notification --- .../NotificationRowView.swift | 156 +++++++++++------- .../NotificationRowViewModel.swift | 27 ++- 2 files changed, 109 insertions(+), 74 deletions(-) diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift index 56742f827..f12401acb 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift @@ -254,14 +254,11 @@ enum RelationshipElement: Equatable { case unfetched(Mastodon.Entity.NotificationType, accountID: String) case fetching case error(Error?) - case followButton - case requestButton - case acceptRejectButtons(isFollowing: Bool) - case acceptedLabel - case rejectedLabel - case mutualButton - case followingButton - case pendingRequestButton + case iDoNotFollowThem(theirAccountIsLocked: Bool) + case iFollowThem(theyFollowMe: Bool) + case iHaveRequestedToFollowThem + case theyHaveRequestedToFollowMe(iFollowThem: Bool) + case iHaveAnsweredTheirRequestToFollowMe(didAccept: Bool) enum FollowAction { case follow @@ -279,22 +276,32 @@ enum RelationshipElement: Equatable { return "fetching" case .error: return "error" - case .followButton: - return "follow" - case .requestButton: - return "request" - case .acceptRejectButtons: - return "acceptReject" - case .acceptedLabel: - return "accepted" - case .rejectedLabel: - return "rejected" - case .mutualButton: - return "mutual" - case .followingButton: - return "following" - case .pendingRequestButton: - return "pending" + case .iDoNotFollowThem(let theirAccountIsLocked): + if theirAccountIsLocked { + return "iDoNotFollowThem+canRequestToFollow" + } else { + return "iDoNotFollowThem+canFollow" + } + case .theyHaveRequestedToFollowMe(let iFollowThem): + if iFollowThem { + return "theyHaveRequestedToFollowMe+iFollowThem" + } else { + return "theyHaveRequestedToFollowMe+iDoNotFollowThem" + } + case .iHaveAnsweredTheirRequestToFollowMe(let didAccept): + if didAccept { + return "iAcceptedTheirFollowRequest" + } else { + return "iRejectedTheirFollowRequest" + } + case .iFollowThem(let theyFollowMe): + if theyFollowMe { + return "iFollowThem+theyFollowMe" + } else { + return "iFollowThem+theyDoNotFollowMe" + } + case .iHaveRequestedToFollowThem: + return "iHaveRequestedToFollowThem" } } @@ -305,9 +312,9 @@ enum RelationshipElement: Equatable { var followAction: FollowAction { switch self { - case .followButton, .requestButton: + case .iDoNotFollowThem: return .follow - case .followingButton, .mutualButton, .pendingRequestButton: + case .iFollowThem, .iHaveRequestedToFollowThem: return .unfollow default: return .noAction @@ -316,15 +323,19 @@ enum RelationshipElement: Equatable { 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: + case .iDoNotFollowThem(let theirAccountIsLocked): + if theirAccountIsLocked { + return L10n.Common.Controls.Friendship.request + } else { + return L10n.Common.Controls.Friendship.follow + } + case .iFollowThem(let theyFollowMe): + if theyFollowMe { + return L10n.Common.Controls.Friendship.mutual + } else { + return L10n.Common.Controls.Friendship.following + } + case .iHaveRequestedToFollowThem: return L10n.Common.Controls.Friendship.pending default: return nil @@ -336,10 +347,8 @@ extension Mastodon.Entity.Relationship { @MainActor var relationshipElement: RelationshipElement? { switch (following, followedBy) { - case (true, true): - return .mutualButton - case (true, false): - return .followingButton + case (true, _): + return .iFollowThem(theyFollowMe: followedBy) case (false, true): if let account: NotificationAuthor = MastodonFeedItemCacheManager .shared.fullAccount(id) @@ -347,12 +356,12 @@ extension Mastodon.Entity.Relationship { account.locked { if requested { - return .pendingRequestButton + return .iHaveRequestedToFollowThem } else { - return .requestButton + return .iDoNotFollowThem(theirAccountIsLocked: true) } } - return .followButton + return .iDoNotFollowThem(theirAccountIsLocked: false) case (false, false): return nil } @@ -592,41 +601,47 @@ struct NotificationRowView: View { switch (elementType, grouped) { case (.fetching, false): ProgressView().progressViewStyle(.circular) - case (.followButton, false), (.mutualButton, false), - (.followingButton, false), (.pendingRequestButton, false): + case (.iDoNotFollowThem, false), (.iFollowThem, false), (.iHaveRequestedToFollowThem, false): if let buttonText = elementType.buttonText { Button(buttonText) { viewModel.doAvatarRowButtonAction() } .buttonStyle(FollowButton(elementType)) } - case (.acceptRejectButtons(let isFollowing), false): + case (.theyHaveRequestedToFollowMe(let iFollowThem), false): HStack { - if isFollowing { - Text(L10n.Common.Controls.Friendship.following) + if iFollowThem { + Button(L10n.Common.Controls.Friendship.following) + { + // TODO: allow unfollow here? + } + .buttonStyle(FollowButton(.iFollowThem(theyFollowMe: false))) + .fixedSize() } - Button(L10n.Scene.Notification.FollowRequest.reject) { + Button(action: { viewModel.doAvatarRowButtonAction(false) + }) { + lightwieghtImageView("xmark.circle", size: smallAvatarSize) } - .buttonStyle(.borderedProminent) - .controlSize(.small) - .bold() + .buttonStyle(ImageButton(foregroundColor: .secondary, backgroundColor: .clear)) - Button(L10n.Scene.Notification.FollowRequest.accept) { + Button(action: { viewModel.doAvatarRowButtonAction(true) + }) { + lightwieghtImageView("checkmark.circle", size: smallAvatarSize) } - .buttonStyle(.borderedProminent) - .controlSize(.small) - .bold() + .buttonStyle(ImageButton(foregroundColor: .secondary, backgroundColor: .clear)) + } + case (.iHaveAnsweredTheirRequestToFollowMe(let didAccept), false): + if didAccept { + lightwieghtImageView("checkmark", size: smallAvatarSize) + } else { + lightwieghtImageView("xmark", size: smallAvatarSize) } - case (.acceptedLabel, false): - Text(L10n.Scene.Notification.FollowRequest.accepted) - case (.rejectedLabel, false): - Text(L10n.Scene.Notification.FollowRequest.rejected) case (.error(_), _): - Image(systemName: "exclamationmark.triangle").foregroundStyle(.gray) + lightwieghtImageView("exclamationmark.triangle", size: smallAvatarSize) default: Spacer().frame(width: 0) } @@ -927,3 +942,24 @@ struct FollowButton: ButtonStyle { } } } + +struct ImageButton: ButtonStyle { + + let foregroundColor: Color + let backgroundColor: Color + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundStyle(foregroundColor) + .background(backgroundColor) + .clipShape(Capsule()) + } +} + +@ViewBuilder func lightwieghtImageView(_ systemName: String, size: CGFloat) -> some View { + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + .fontWeight(.light) + .frame(width: size, height: size) +} diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift index 008897f70..4cbcd3b9f 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift @@ -226,7 +226,7 @@ class NotificationRowViewModel: ObservableObject { ) { switch type { case .follow, .followRequest: - guard let accountID = sourceAccounts.firstAccountID else { return } + guard let accountID = sourceAccounts.firstAccountID, let accountIsLocked = sourceAccounts.primaryAuthorAccount?.locked else { return } avatarRow = .avatarRow(sourceAccounts, .fetching) Task { @MainActor in @@ -238,12 +238,11 @@ class NotificationRowViewModel: ObservableObject { switch (type, relationship.following) { case (.follow, true): - element = .mutualButton + element = .iFollowThem(theyFollowMe: true) case (.follow, false): - element = .followButton + element = .iDoNotFollowThem(theirAccountIsLocked: accountIsLocked) case (.followRequest, _): - element = .acceptRejectButtons( - isFollowing: relationship.following) + element = .theyHaveRequestedToFollowMe(iFollowThem: relationship.following) default: element = .noneNeeded } @@ -312,10 +311,10 @@ extension NotificationRowViewModel { switch avatarRow { case .avatarRow(let accountInfo, let relationshipElement): switch relationshipElement { - case .followButton, .requestButton, .mutualButton, .followingButton, .pendingRequestButton: + case .iDoNotFollowThem, .iFollowThem, .iHaveRequestedToFollowThem: await doFollowAction(relationshipElement.followAction, notificationSourceAccounts: accountInfo) - case .acceptRejectButtons: - await doAcceptFollowRequest(accountInfo, accept: accept) + case .theyHaveRequestedToFollowMe: + await doAnswerFollowRequest(accountInfo, accept: accept) default: return } @@ -327,7 +326,7 @@ extension NotificationRowViewModel { @MainActor private func doFollowAction(_ action: RelationshipElement.FollowAction, notificationSourceAccounts: NotificationSourceAccounts) async { - guard let accountID = notificationSourceAccounts.firstAccountID, + guard let accountID = notificationSourceAccounts.firstAccountID, let theirAccountIsLocked = notificationSourceAccounts.primaryAuthorAccount?.locked, let authBox = AuthenticationServiceProvider.shared.currentActiveUser .value else { return } @@ -346,11 +345,11 @@ extension NotificationRowViewModel { throw AppError.unexpected("action attempted for relationship element that has no action") } if response.following { - updatedElement = .followingButton + updatedElement = .iFollowThem(theyFollowMe: response.followedBy) } else if response.requested { - updatedElement = .pendingRequestButton + updatedElement = .iHaveRequestedToFollowThem } else { - updatedElement = .followButton + updatedElement = .iDoNotFollowThem(theirAccountIsLocked: theirAccountIsLocked) } avatarRow = .avatarRow(notificationSourceAccounts, updatedElement) } catch { @@ -360,7 +359,7 @@ extension NotificationRowViewModel { } @MainActor - private func doAcceptFollowRequest( + private func doAnswerFollowRequest( _ accountInfo: NotificationSourceAccounts, accept: Bool ) async { guard let accountID = accountInfo.firstAccountID, @@ -381,7 +380,7 @@ extension NotificationRowViewModel { return } self.avatarRow = .avatarRow( - accountInfo, accept ? .acceptedLabel : .rejectedLabel) + accountInfo, .iHaveAnsweredTheirRequestToFollowMe(didAccept: accept)) } catch { presentError(error) self.avatarRow = startingAvatarRow