2
2
mirror of https://github.com/mastodon/mastodon-ios synced 2025-04-11 22:58:02 +02:00

Add label above notification to call attention to public vs. private mentions and replies

Contributes to #399 [BUG] Multiple interactions do not collapse into a single notification
This commit is contained in:
shannon 2025-02-18 14:02:27 -05:00
parent 4847c461a0
commit c85bbad148
5 changed files with 76 additions and 9 deletions

View File

@ -156,6 +156,10 @@
}
},
"status": {
"reply": "Reply",
"private_reply": "Private reply",
"mention": "Mention",
"private_mention": "Private mention",
"user_reblogged": "%s boosted",
"user_replied_to": "Replied to %s",
"show_post": "Show Post",

View File

@ -633,15 +633,27 @@ struct FilteredNotificationsRowView: View {
}
}
let actionSuperheaderHeight: CGFloat = 20
struct NotificationRowView: View {
@ObservedObject var viewModel: NotificationRowViewModel
var body: some View {
HStack {
HStack(alignment: .top) {
if let iconStyle = viewModel.iconStyle {
// LEFT GUTTER WITH TOP-ALIGNED ICON or AVATAR
VStack {
Spacer()
VStack(spacing: 4) {
if let actionSuperheader = viewModel.actionSuperheader {
HStack {
Spacer()
Image(systemName: actionSuperheader.iconName)
.font(.subheadline)
.bold()
.foregroundStyle(actionSuperheader.color)
.frame(height: actionSuperheaderHeight)
}
}
switch iconStyle {
case .icon:
NotificationIconView(iconStyle)
@ -653,10 +665,18 @@ struct NotificationRowView: View {
}
Spacer().frame(maxHeight: .infinity)
}
.fixedSize(horizontal: true, vertical: false)
}
// VSTACK OF HEADER AND CONTENT COMPONENT VIEWS
VStack(spacing: 4) {
if let actionSuperheader = viewModel.actionSuperheader {
componentView(.weightedText(actionSuperheader.text, .bold))
.font(.subheadline)
.foregroundColor(actionSuperheader.color)
.frame(height: actionSuperheaderHeight)
}
ForEach(viewModel.headerComponents) {
componentView($0)
}

View File

@ -2,9 +2,11 @@
import Combine
import Foundation
import MastodonAsset
import MastodonCore
import MastodonLocalization
import MastodonSDK
import SwiftUICore
class NotificationRowViewModel: ObservableObject {
let identifier: MastodonFeedItemIdentifier
@ -16,7 +18,8 @@ class NotificationRowViewModel: ObservableObject {
(SceneCoordinator.Scene, SceneCoordinator.Transition) -> Void
let presentError: (Error) -> Void
let defaultNavigation: (() -> Void)?
public let iconStyle: GroupedNotificationType.MainIconStyle?
let iconStyle: GroupedNotificationType.MainIconStyle?
let actionSuperheader: (iconName: String, text: String, color: Color)?
@Published public var headerComponents: [NotificationViewComponent] = []
public var contentComponents: [NotificationViewComponent] = []
@ -59,9 +62,10 @@ class NotificationRowViewModel: ObservableObject {
switch notificationInfo.groupedNotificationType {
case .follow, .followRequest:
actionSuperheader = nil
let avatarRowAdditionalElement: RelationshipElement
if let account = notificationInfo.sourceAccounts
.primaryAuthorAccount
if notificationInfo.sourceAccounts
.primaryAuthorAccount != nil
{
avatarRowAdditionalElement = .unfetched(
notificationInfo.groupedNotificationType)
@ -71,9 +75,9 @@ class NotificationRowViewModel: ObservableObject {
avatarRow = .avatarRow(
notificationInfo.sourceAccounts,
avatarRowAdditionalElement)
if let accountName = notificationInfo.sourceAccounts
if (notificationInfo.sourceAccounts
.primaryAuthorAccount?
.displayNameWithFallback
.displayNameWithFallback) != nil
{
headerTextComponents = [
.text(
@ -84,10 +88,10 @@ class NotificationRowViewModel: ObservableObject {
}
case .mention, .status:
// TODO: eventually make this full status style, not inline
// TODO: distinguish mentions from replies
if let statusViewModel =
notificationInfo.statusViewModel
{
actionSuperheader = NotificationRowViewModel.actionSuperheader(notificationInfo.groupedNotificationType, isReply: statusViewModel.isReply, isPrivateStatus: statusViewModel.visibility == .direct)
headerTextComponents = [
.text(
notificationInfo.groupedNotificationType
@ -96,9 +100,11 @@ class NotificationRowViewModel: ObservableObject {
]
contentComponents = [.status(statusViewModel)]
} else {
actionSuperheader = nil
headerTextComponents = [._other("POST BY UNKNOWN ACCOUNT")]
}
case .reblog, .favourite:
actionSuperheader = nil
if let statusViewModel = notificationInfo.statusViewModel {
avatarRow = .avatarRow(
notificationInfo.sourceAccounts,
@ -116,6 +122,7 @@ class NotificationRowViewModel: ObservableObject {
]
}
case .poll, .update:
actionSuperheader = nil
if let statusViewModel =
notificationInfo.statusViewModel
{
@ -132,6 +139,7 @@ class NotificationRowViewModel: ObservableObject {
]
}
case .adminSignUp:
actionSuperheader = nil
avatarRow = .avatarRow(
notificationInfo.sourceAccounts,
.noneNeeded)
@ -141,6 +149,7 @@ class NotificationRowViewModel: ObservableObject {
notificationInfo.sourceAccounts) ?? "")
]
case .adminReport(let report):
actionSuperheader = nil
if let summary = report?.summary {
headerTextComponents = [.text(summary)]
}
@ -150,6 +159,7 @@ class NotificationRowViewModel: ObservableObject {
contentComponents = [.text(comment)]
}
case .severedRelationships(let severanceEvent):
actionSuperheader = nil
if let summary = severanceEvent?.summary(myDomain: myAccountDomain)
{
headerTextComponents = [.text(summary)]
@ -168,6 +178,7 @@ class NotificationRowViewModel: ObservableObject {
notificationID: notificationInfo.newestNotificationID))
]
case .moderationWarning(let accountWarning):
actionSuperheader = nil
headerTextComponents = [
.weightedText(
(accountWarning?.action ?? .none).actionDescription,
@ -192,12 +203,32 @@ class NotificationRowViewModel: ObservableObject {
}
case ._other(let text):
actionSuperheader = nil
headerTextComponents = [
._other("UNEXPECTED NOTIFICATION TYPE: \(text)")
]
}
resetHeaderComponents()
}
static func actionSuperheader(_ notificationType: GroupedNotificationType, isReply: Bool, isPrivateStatus: Bool?) -> (iconName: String, text: String, color: Color)? {
guard let isPrivateStatus else { return nil }
switch notificationType {
case .mention:
switch (isReply, isPrivateStatus) {
case (true, false):
return (iconName: "arrow.turn.up.left", text: L10n.Common.Controls.Status.reply, color: .gray)
case (true, true):
return (iconName: "arrow.turn.up.left", text: L10n.Common.Controls.Status.privateReply, color: Asset.Colors.accent.swiftUIColor)
case (false, false):
return (iconName: "at", text: L10n.Common.Controls.Status.mention, color: .gray)
case (false, true):
return (iconName: "at", text: L10n.Common.Controls.Status.privateMention, color: Asset.Colors.accent.swiftUIColor)
}
default:
return nil
}
}
public func prepareForDisplay() {
if let avatarRow {

View File

@ -347,10 +347,18 @@ public enum L10n {
public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed")
/// Tap anywhere to reveal
public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal")
/// Mention
public static let mention = L10n.tr("Localizable", "Common.Controls.Status.Mention", fallback: "Mention")
/// %@ via %@
public static func postedViaApplication(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "Common.Controls.Status.PostedViaApplication", String(describing: p1), String(describing: p2), fallback: "%@ via %@")
}
/// Private mention
public static let privateMention = L10n.tr("Localizable", "Common.Controls.Status.PrivateMention", fallback: "Private mention")
/// Private reply
public static let privateReply = L10n.tr("Localizable", "Common.Controls.Status.PrivateReply", fallback: "Private reply")
/// Reply
public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Reply", fallback: "Reply")
/// Sensitive Content
public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent", fallback: "Sensitive Content")
/// Show Post

View File

@ -171,6 +171,10 @@ Please check your internet connection.";
"Common.Controls.Status.Translation.TranslatedFrom" = "Translated from %@ using %@";
"Common.Controls.Status.Translation.UnknownLanguage" = "Unknown";
"Common.Controls.Status.Translation.UnknownProvider" = "Unknown";
"Common.Controls.Status.Reply" = "Reply";
"Common.Controls.Status.PrivateReply" = "Private reply";
"Common.Controls.Status.Mention" = "Mention";
"Common.Controls.Status.PrivateMention" = "Private mention";
"Common.Controls.Status.UserReblogged" = "%@ boosted";
"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
"Common.Controls.Status.Visibility.Direct" = "Only mentioned user can see this post.";