diff --git a/Localization/app.json b/Localization/app.json index 7d5a1cd92..bbf01ab4c 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -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", diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift index 9fffd1dd7..d9f147aab 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationRowView.swift @@ -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) } diff --git a/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift b/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift index 355f85cc6..9091c3037 100644 --- a/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift +++ b/Mastodon/In Progress New Layout and Datamodel/NotificationRowViewModel.swift @@ -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 { diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index b2b6431b2..9a71a60a1 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -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 diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index f41eb554a..332a90614 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -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.";