mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
Add url actions and begin localizing grouped notification display
With refactoring to make that easier and more readable. Contributes to #399 [BUG] Multiple interactions do not collapse into a single notification
This commit is contained in:
parent
f2311cc0fd
commit
6a27b50728
@ -8,12 +8,11 @@ protocol NotificationInfo {
|
||||
var id: String { get }
|
||||
var newestNotificationID: String { get }
|
||||
var oldestNotificationID: String { get }
|
||||
var type: Mastodon.Entity.NotificationType { get }
|
||||
var typeFromServer: Mastodon.Entity.NotificationType { get }
|
||||
var isGrouped: Bool { get }
|
||||
var notificationsCount: Int { get }
|
||||
var authorsCount: Int { get }
|
||||
var primaryAuthorAccount: Mastodon.Entity.Account? { get }
|
||||
var authorName: Mastodon.Entity.NotificationType.AuthorName? { get }
|
||||
var authorAvatarUrls: [URL] { get }
|
||||
func availableRelationshipElement() async -> RelationshipElement?
|
||||
func fetchRelationshipElement() async -> RelationshipElement
|
||||
@ -21,28 +20,26 @@ protocol NotificationInfo {
|
||||
var relationshipSeveranceEvent: Mastodon.Entity.RelationshipSeveranceEvent?
|
||||
{ get }
|
||||
}
|
||||
extension NotificationInfo {
|
||||
var authorsDescription: String? {
|
||||
switch authorName {
|
||||
case .me, .none:
|
||||
return nil
|
||||
case .other(let name):
|
||||
if authorsCount > 1 {
|
||||
return "\(name) and \(authorsCount - 1) others"
|
||||
} else {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
var avatarCount: Int {
|
||||
min(authorsCount, 8)
|
||||
}
|
||||
var isGrouped: Bool {
|
||||
return authorsCount > 1
|
||||
}
|
||||
|
||||
enum GroupedNotificationType {
|
||||
// TODO: update to use StatusViewModel rather than Status
|
||||
case follow(from: NotificationSourceAccounts) // Someone followed you
|
||||
case followRequest(from: Mastodon.Entity.Account) // Someone requested to follow you
|
||||
case mention(Mastodon.Entity.Status?) // Someone mentioned you in their status
|
||||
case reblog(Mastodon.Entity.Status?) // Someone boosted one of your statuses
|
||||
case favourite(Mastodon.Entity.Status?) // Someone favourited one of your statuses
|
||||
case poll(Mastodon.Entity.Status?) // A poll you have voted in or created has ended
|
||||
case status(Mastodon.Entity.Status?) // Someone you enabled notifications for has posted a status
|
||||
case update(Mastodon.Entity.Status?) // A status you interacted with has been edited
|
||||
case adminSignUp // Someone signed up (optionally sent to admins)
|
||||
case adminReport(Mastodon.Entity.Report?) // A new report has been filed
|
||||
case severedRelationships(Mastodon.Entity.RelationshipSeveranceEvent?) // Some of your follow relationships have been severed as a result of a moderation or block event
|
||||
case moderationWarning(Mastodon.Entity.AccountWarning?) // A moderator has taken action against your account or has sent you a warning
|
||||
|
||||
case _other(String)
|
||||
}
|
||||
|
||||
struct GroupedNotificationInfo: NotificationInfo {
|
||||
struct GroupedNotificationInfo {
|
||||
func availableRelationshipElement() async -> RelationshipElement? {
|
||||
return relationshipElement
|
||||
}
|
||||
@ -55,23 +52,21 @@ struct GroupedNotificationInfo: NotificationInfo {
|
||||
let oldestNotificationID: String
|
||||
let newestNotificationID: String
|
||||
|
||||
let type: MastodonSDK.Mastodon.Entity.NotificationType
|
||||
let groupedNotificationType: GroupedNotificationType
|
||||
|
||||
let authorsCount: Int
|
||||
|
||||
let notificationsCount: Int
|
||||
|
||||
let primaryAuthorAccount: MastodonSDK.Mastodon.Entity.Account?
|
||||
|
||||
let authorName: Mastodon.Entity.NotificationType.AuthorName?
|
||||
|
||||
let authorAvatarUrls: [URL]
|
||||
let sourceAccounts: NotificationSourceAccounts
|
||||
|
||||
var relationshipElement: RelationshipElement {
|
||||
switch type {
|
||||
case .follow, .followRequest:
|
||||
if let primaryAuthorAccount {
|
||||
return .unfetched(type, accountID: primaryAuthorAccount.id)
|
||||
switch groupedNotificationType {
|
||||
case .follow(let accountsInfo):
|
||||
if let primaryAuthorAccount = accountsInfo.primaryAuthorAccount {
|
||||
return .unfetched(groupedNotificationType)
|
||||
} else {
|
||||
return .error(nil)
|
||||
}
|
||||
case .followRequest(let account):
|
||||
if sourceAccounts.totalActorCount == 1 {
|
||||
return .unfetched(groupedNotificationType)
|
||||
} else {
|
||||
return .error(nil)
|
||||
}
|
||||
@ -81,13 +76,14 @@ struct GroupedNotificationInfo: NotificationInfo {
|
||||
}
|
||||
|
||||
let statusViewModel: Mastodon.Entity.Status.ViewModel?
|
||||
let ruleViolationReport: Mastodon.Entity.Report?
|
||||
let relationshipSeveranceEvent: Mastodon.Entity.RelationshipSeveranceEvent?
|
||||
|
||||
let defaultNavigation: (() -> Void)?
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Notification: NotificationInfo {
|
||||
var isGrouped: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var oldestNotificationID: String {
|
||||
return id
|
||||
@ -96,12 +92,14 @@ extension Mastodon.Entity.Notification: NotificationInfo {
|
||||
return id
|
||||
}
|
||||
|
||||
var typeFromServer: Mastodon.Entity.NotificationType {
|
||||
return type
|
||||
}
|
||||
|
||||
var authorsCount: Int { 1 }
|
||||
var notificationsCount: Int { 1 }
|
||||
var primaryAuthorAccount: Mastodon.Entity.Account? { account }
|
||||
var authorName: Mastodon.Entity.NotificationType.AuthorName? {
|
||||
.other(named: account.displayNameWithFallback)
|
||||
}
|
||||
|
||||
var authorAvatarUrls: [URL] {
|
||||
if let domain = account.domain {
|
||||
return [account.avatarImageURLWithFallback(domain: domain)]
|
||||
@ -146,101 +144,3 @@ extension Mastodon.Entity.Notification: NotificationInfo {
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.NotificationGroup: NotificationInfo {
|
||||
|
||||
var newestNotificationID: String {
|
||||
return pageNewestID ?? "\(mostRecentNotificationID)"
|
||||
}
|
||||
var oldestNotificationID: String {
|
||||
return pageOldestID ?? "\(mostRecentNotificationID)"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var primaryAuthorAccount: Mastodon.Entity.Account? {
|
||||
guard let firstAccountID = sampleAccountIDs.first else { return nil }
|
||||
return MastodonFeedItemCacheManager.shared.fullAccount(firstAccountID)
|
||||
}
|
||||
|
||||
var authorsCount: Int { notificationsCount }
|
||||
|
||||
@MainActor
|
||||
var authorName: Mastodon.Entity.NotificationType.AuthorName? {
|
||||
guard let firstAccountID = sampleAccountIDs.first,
|
||||
let firstAccount = MastodonFeedItemCacheManager.shared.fullAccount(
|
||||
firstAccountID)
|
||||
else { return .none }
|
||||
return .other(named: firstAccount.displayNameWithFallback)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var authorAvatarUrls: [URL] {
|
||||
return
|
||||
sampleAccountIDs
|
||||
.prefix(avatarCount)
|
||||
.compactMap { accountID in
|
||||
let account: NotificationAuthor? =
|
||||
MastodonFeedItemCacheManager.shared.fullAccount(accountID)
|
||||
?? MastodonFeedItemCacheManager.shared.partialAccount(
|
||||
accountID)
|
||||
return account?.avatarURL
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var firstAccount: NotificationAuthor? {
|
||||
guard let firstAccountID = sampleAccountIDs.first else { return nil }
|
||||
let firstAccount: NotificationAuthor? =
|
||||
MastodonFeedItemCacheManager.shared.fullAccount(firstAccountID)
|
||||
?? MastodonFeedItemCacheManager.shared.partialAccount(
|
||||
firstAccountID)
|
||||
return firstAccount
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func availableRelationshipElement() -> RelationshipElement? {
|
||||
guard authorsCount == 1 && type == .follow else { return .noneNeeded }
|
||||
guard let firstAccountID = sampleAccountIDs.first else {
|
||||
return .noneNeeded
|
||||
}
|
||||
if let relationship = MastodonFeedItemCacheManager.shared
|
||||
.currentRelationship(toAccount: firstAccountID)
|
||||
{
|
||||
return relationship.relationshipElement
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func fetchRelationshipElement() async -> RelationshipElement {
|
||||
do {
|
||||
try await fetchRelationship()
|
||||
if let available = availableRelationshipElement() {
|
||||
return available
|
||||
} else {
|
||||
return .noneNeeded
|
||||
}
|
||||
} catch {
|
||||
return .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchRelationship() async throws {
|
||||
assert(
|
||||
notificationsCount == 1,
|
||||
"one relationship cannot be assumed representative of \(notificationsCount) notifications"
|
||||
)
|
||||
guard let firstAccountId = sampleAccountIDs.first,
|
||||
let authBox = await AuthenticationServiceProvider.shared
|
||||
.currentActiveUser.value
|
||||
else { return }
|
||||
if let relationship = try await APIService.shared.relationship(
|
||||
forAccountIds: [firstAccountId], authenticationBox: authBox
|
||||
).value.first {
|
||||
await MastodonFeedItemCacheManager.shared.addToCache(relationship)
|
||||
}
|
||||
}
|
||||
|
||||
var statusViewModel: MastodonSDK.Mastodon.Entity.Status.ViewModel? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -78,19 +78,6 @@ extension Mastodon.Entity.NotificationType {
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthorName {
|
||||
case me
|
||||
case other(named: String)
|
||||
|
||||
var string: String {
|
||||
switch self {
|
||||
case .me:
|
||||
return "You"
|
||||
case .other(let name):
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
func actionSummaryLabel(firstAuthor: AuthorName, totalAuthorCount: Int)
|
||||
-> AttributedString
|
||||
{
|
||||
@ -169,6 +156,167 @@ extension Mastodon.Entity.NotificationType {
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthorName {
|
||||
case me
|
||||
case other(named: String)
|
||||
|
||||
var string: String {
|
||||
switch self {
|
||||
case .me:
|
||||
return "You"
|
||||
case .other(let name):
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupedNotificationType {
|
||||
func shouldShowIcon(
|
||||
grouped: Bool, visibility: Mastodon.Entity.Status.Visibility?
|
||||
) -> Bool {
|
||||
return iconSystemName(grouped: grouped, visibility: visibility) != nil
|
||||
}
|
||||
|
||||
func iconSystemName(
|
||||
grouped: Bool = false, visibility: Mastodon.Entity.Status.Visibility?
|
||||
) -> String? {
|
||||
switch self {
|
||||
case .favourite:
|
||||
return "star.fill"
|
||||
case .reblog:
|
||||
return "arrow.2.squarepath"
|
||||
case .follow:
|
||||
if grouped {
|
||||
return "person.2.badge.plus.fill"
|
||||
} else {
|
||||
return "person.fill.badge.plus"
|
||||
}
|
||||
case .poll:
|
||||
return "chart.bar.yaxis"
|
||||
case .adminReport:
|
||||
return "info.circle"
|
||||
case .severedRelationships:
|
||||
return "person.badge.minus"
|
||||
case .moderationWarning:
|
||||
return "exclamationmark.shield.fill"
|
||||
case ._other:
|
||||
return "questionmark.square.dashed"
|
||||
case .mention:
|
||||
// TODO: make this nil when full status view is available
|
||||
switch visibility {
|
||||
case .direct:
|
||||
return "at.circle.fill"
|
||||
default:
|
||||
return "at"
|
||||
}
|
||||
case .status:
|
||||
// TODO: make this nil when full status view is available
|
||||
return "bell.fill"
|
||||
case .followRequest:
|
||||
return "person.fill.badge.plus"
|
||||
case .update:
|
||||
return "pencil"
|
||||
case .adminSignUp:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var iconColor: Color {
|
||||
switch self {
|
||||
case .favourite:
|
||||
return .orange
|
||||
case .reblog:
|
||||
return .green
|
||||
case .follow, .followRequest, .status, .mention, .update:
|
||||
return Color(asset: Asset.Colors.accent)
|
||||
case .poll, .severedRelationships, .moderationWarning, .adminReport,
|
||||
.adminSignUp:
|
||||
return .secondary
|
||||
case ._other:
|
||||
return .gray
|
||||
}
|
||||
}
|
||||
|
||||
func actionSummaryLabel(_ sourceAccounts: NotificationSourceAccounts)
|
||||
-> AttributedString?
|
||||
{
|
||||
guard let authorName = sourceAccounts.authorName else { return nil }
|
||||
let totalAuthorCount = sourceAccounts.totalActorCount
|
||||
// TODO: L10n strings
|
||||
switch authorName {
|
||||
case .me:
|
||||
assert(totalAuthorCount == 1)
|
||||
//assert(self == .poll)
|
||||
return "Your poll has ended"
|
||||
case .other(let firstAuthorName):
|
||||
let nameComponent = boldedNameStringComponent(firstAuthorName)
|
||||
var composedString: AttributedString
|
||||
if totalAuthorCount == 1 {
|
||||
switch self {
|
||||
case .favourite:
|
||||
composedString =
|
||||
nameComponent + AttributedString(" favorited:")
|
||||
case .follow:
|
||||
composedString =
|
||||
nameComponent + AttributedString(" followed you")
|
||||
case .followRequest:
|
||||
composedString =
|
||||
nameComponent
|
||||
+ AttributedString(" requested to follow you")
|
||||
case .reblog:
|
||||
composedString =
|
||||
nameComponent + AttributedString(" boosted:")
|
||||
case .mention:
|
||||
composedString =
|
||||
nameComponent + AttributedString(" mentioned you:")
|
||||
case .poll:
|
||||
composedString =
|
||||
nameComponent
|
||||
+ AttributedString(" ran a poll that you voted in") // TODO: add count of how many others voted
|
||||
case .status:
|
||||
composedString =
|
||||
nameComponent + AttributedString(" posted:")
|
||||
case .adminSignUp:
|
||||
composedString =
|
||||
nameComponent + AttributedString(" signed up")
|
||||
default:
|
||||
composedString =
|
||||
nameComponent + AttributedString("did something?")
|
||||
}
|
||||
} else {
|
||||
switch self {
|
||||
case .favourite:
|
||||
composedString =
|
||||
nameComponent
|
||||
+ AttributedString(
|
||||
" and \(totalAuthorCount - 1) others favorited:")
|
||||
case .follow:
|
||||
composedString =
|
||||
nameComponent
|
||||
+ AttributedString(
|
||||
" and \(totalAuthorCount - 1) others followed you")
|
||||
case .reblog:
|
||||
composedString =
|
||||
nameComponent
|
||||
+ AttributedString(
|
||||
" and \(totalAuthorCount - 1) others boosted:")
|
||||
default:
|
||||
composedString =
|
||||
nameComponent
|
||||
+ AttributedString(
|
||||
" and \(totalAuthorCount - 1) others did something")
|
||||
}
|
||||
}
|
||||
let nameStyling = AttributeContainer.font(
|
||||
.system(.body, weight: .bold))
|
||||
let nameContainer = AttributeContainer.personNameComponent(
|
||||
.givenName)
|
||||
composedString.replaceAttributes(nameContainer, with: nameStyling)
|
||||
return composedString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Report {
|
||||
// TODO: localization (inc. plurals)
|
||||
// "Someone reported X posts from someone else for rule violation"
|
||||
@ -199,16 +347,16 @@ extension Mastodon.Entity.Report {
|
||||
var listFormatter = ListFormatter()
|
||||
|
||||
extension Mastodon.Entity.RelationshipSeveranceEvent {
|
||||
// TODO: details and localization
|
||||
// Ideal example: "An admin from a.b has blocked c.d, including x of your followers and y accounts you follow."
|
||||
// For now: "An admin action has blocked x of your followers and y accounts that you follow"
|
||||
var summary: AttributedString? {
|
||||
let baseString = "Your admins have blocked "
|
||||
// "An admin from <your.domain> has blocked <some other domain>, including x of your followers and y accounts you follow."
|
||||
|
||||
func summary(myDomain: String) -> AttributedString? {
|
||||
let lostFollowersString =
|
||||
followersCount > 0 ? "\(followersCount) of your followers" : nil
|
||||
followersCount > 0
|
||||
? L10n.Plural.Count.ofYourFollowers(followersCount) : nil
|
||||
let lostFollowingString =
|
||||
followingCount > 0
|
||||
? "\(followingCount) accounts that you follow" : nil
|
||||
? L10n.Plural.Count.accountsThatYouFollow(followingCount) : nil
|
||||
|
||||
guard
|
||||
let followersAndFollowingString = listFormatter.string(
|
||||
from: [lostFollowersString, lostFollowingString].compactMap {
|
||||
@ -217,7 +365,14 @@ extension Mastodon.Entity.RelationshipSeveranceEvent {
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return AttributedString(baseString + followersAndFollowingString + ".")
|
||||
|
||||
let string = L10n.Scene.Notification.NotificationDescription
|
||||
.relationshipSeveranceEvent(
|
||||
myDomain, targetName, followersAndFollowingString)
|
||||
|
||||
var attributed = AttributedString(string)
|
||||
attributed.bold([myDomain, targetName])
|
||||
return attributed
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,7 +406,7 @@ func NotificationIconView(systemName: String) -> some View {
|
||||
|
||||
enum RelationshipElement: Equatable {
|
||||
case noneNeeded
|
||||
case unfetched(Mastodon.Entity.NotificationType, accountID: String)
|
||||
case unfetched(GroupedNotificationType)
|
||||
case fetching
|
||||
case error(Error?)
|
||||
case iDoNotFollowThem(theirAccountIsLocked: Bool)
|
||||
@ -259,7 +414,7 @@ enum RelationshipElement: Equatable {
|
||||
case iHaveRequestedToFollowThem
|
||||
case theyHaveRequestedToFollowMe(iFollowThem: Bool)
|
||||
case iHaveAnsweredTheirRequestToFollowMe(didAccept: Bool)
|
||||
|
||||
|
||||
enum FollowAction {
|
||||
case follow
|
||||
case unfollow
|
||||
@ -309,7 +464,7 @@ enum RelationshipElement: Equatable {
|
||||
{
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
|
||||
var followAction: FollowAction {
|
||||
switch self {
|
||||
case .iDoNotFollowThem:
|
||||
@ -369,13 +524,14 @@ extension Mastodon.Entity.Relationship {
|
||||
}
|
||||
|
||||
struct NotificationIconInfo {
|
||||
let notificationType: Mastodon.Entity.NotificationType
|
||||
let notificationType: GroupedNotificationType
|
||||
let isGrouped: Bool
|
||||
let visibility: Mastodon.Entity.Status.Visibility?
|
||||
}
|
||||
|
||||
struct NotificationSourceAccounts {
|
||||
let primaryAuthorAccount: Mastodon.Entity.Account?
|
||||
let authorName: AuthorName?
|
||||
var firstAccountID: String? {
|
||||
return primaryAuthorAccount?.id
|
||||
}
|
||||
@ -383,13 +539,24 @@ struct NotificationSourceAccounts {
|
||||
let totalActorCount: Int
|
||||
|
||||
init(
|
||||
myAccountID: String,
|
||||
primaryAuthorAccount: Mastodon.Entity.Account?, avatarUrls: [URL],
|
||||
totalActorCount: Int
|
||||
) {
|
||||
self.primaryAuthorAccount = primaryAuthorAccount
|
||||
self.avatarUrls = avatarUrls.removingDuplicates()
|
||||
self.totalActorCount = totalActorCount
|
||||
let authorName: AuthorName?
|
||||
if primaryAuthorAccount?.id == myAccountID {
|
||||
authorName = .me
|
||||
} else if let name = primaryAuthorAccount?.displayNameWithFallback {
|
||||
authorName = .other(named: name)
|
||||
} else {
|
||||
authorName = nil
|
||||
}
|
||||
self.authorName = authorName
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct FilteredNotificationsRowView: View {
|
||||
@ -533,7 +700,9 @@ struct NotificationRowView: View {
|
||||
}
|
||||
case .hyperlinkButton(let label, let url):
|
||||
Button(label) {
|
||||
// TODO: open url
|
||||
if let url {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
.bold()
|
||||
.tint(Color(asset: Asset.Colors.accent))
|
||||
@ -601,7 +770,8 @@ struct NotificationRowView: View {
|
||||
switch (elementType, grouped) {
|
||||
case (.fetching, false):
|
||||
ProgressView().progressViewStyle(.circular)
|
||||
case (.iDoNotFollowThem, false), (.iFollowThem, false), (.iHaveRequestedToFollowThem, false):
|
||||
case (.iDoNotFollowThem, false), (.iFollowThem, false),
|
||||
(.iHaveRequestedToFollowThem, false):
|
||||
if let buttonText = elementType.buttonText {
|
||||
Button(buttonText) {
|
||||
viewModel.doAvatarRowButtonAction()
|
||||
@ -612,11 +782,12 @@ struct NotificationRowView: View {
|
||||
HStack {
|
||||
|
||||
if iFollowThem {
|
||||
Button(L10n.Common.Controls.Friendship.following)
|
||||
{
|
||||
Button(L10n.Common.Controls.Friendship.following) {
|
||||
// TODO: allow unfollow here?
|
||||
}
|
||||
.buttonStyle(FollowButton(.iFollowThem(theyFollowMe: false)))
|
||||
.buttonStyle(
|
||||
FollowButton(.iFollowThem(theyFollowMe: false))
|
||||
)
|
||||
.fixedSize()
|
||||
}
|
||||
|
||||
@ -625,14 +796,19 @@ struct NotificationRowView: View {
|
||||
}) {
|
||||
lightwieghtImageView("xmark.circle", size: smallAvatarSize)
|
||||
}
|
||||
.buttonStyle(ImageButton(foregroundColor: .secondary, backgroundColor: .clear))
|
||||
.buttonStyle(
|
||||
ImageButton(
|
||||
foregroundColor: .secondary, backgroundColor: .clear))
|
||||
|
||||
Button(action: {
|
||||
viewModel.doAvatarRowButtonAction(true)
|
||||
}) {
|
||||
lightwieghtImageView("checkmark.circle", size: smallAvatarSize)
|
||||
lightwieghtImageView(
|
||||
"checkmark.circle", size: smallAvatarSize)
|
||||
}
|
||||
.buttonStyle(ImageButton(foregroundColor: .secondary, backgroundColor: .clear))
|
||||
.buttonStyle(
|
||||
ImageButton(
|
||||
foregroundColor: .secondary, backgroundColor: .clear))
|
||||
}
|
||||
case (.iHaveAnsweredTheirRequestToFollowMe(let didAccept), false):
|
||||
if didAccept {
|
||||
@ -641,7 +817,8 @@ struct NotificationRowView: View {
|
||||
lightwieghtImageView("xmark", size: smallAvatarSize)
|
||||
}
|
||||
case (.error(_), _):
|
||||
lightwieghtImageView("exclamationmark.triangle", size: smallAvatarSize)
|
||||
lightwieghtImageView(
|
||||
"exclamationmark.triangle", size: smallAvatarSize)
|
||||
default:
|
||||
Spacer().frame(width: 0)
|
||||
}
|
||||
@ -890,11 +1067,11 @@ extension Mastodon.Entity.Status {
|
||||
|
||||
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)
|
||||
@ -905,7 +1082,7 @@ struct FollowButton: ButtonStyle {
|
||||
.fontWeight(fontWeight)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
|
||||
private var backgroundColor: Color {
|
||||
switch followAction {
|
||||
case .follow:
|
||||
@ -917,7 +1094,7 @@ struct FollowButton: ButtonStyle {
|
||||
return .clear
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var textColor: Color {
|
||||
switch followAction {
|
||||
case .follow:
|
||||
@ -929,7 +1106,7 @@ struct FollowButton: ButtonStyle {
|
||||
return .clear
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var fontWeight: SwiftUICore.Font.Weight {
|
||||
switch followAction {
|
||||
case .follow:
|
||||
@ -944,10 +1121,10 @@ struct FollowButton: ButtonStyle {
|
||||
}
|
||||
|
||||
struct ImageButton: ButtonStyle {
|
||||
|
||||
|
||||
let foregroundColor: Color
|
||||
let backgroundColor: Color
|
||||
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.foregroundStyle(foregroundColor)
|
||||
@ -956,10 +1133,23 @@ struct ImageButton: ButtonStyle {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func lightwieghtImageView(_ systemName: String, size: CGFloat) -> some View {
|
||||
@ViewBuilder func lightwieghtImageView(_ systemName: String, size: CGFloat)
|
||||
-> some View
|
||||
{
|
||||
Image(systemName: systemName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.fontWeight(.light)
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
|
||||
extension AttributedString {
|
||||
mutating func bold(_ substrings: [String]) {
|
||||
let boldedRanges = substrings.map {
|
||||
self.range(of: $0)
|
||||
}.compactMap { $0 }
|
||||
for range in boldedRanges {
|
||||
self[range].font = .system(.body).bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,14 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
class NotificationRowViewModel: ObservableObject {
|
||||
let identifier: MastodonFeedItemIdentifier
|
||||
let oldestID: String?
|
||||
let newestID: String?
|
||||
let type: Mastodon.Entity.NotificationType
|
||||
let type: GroupedNotificationType
|
||||
let navigateToScene:
|
||||
(SceneCoordinator.Scene, SceneCoordinator.Transition) -> Void
|
||||
let presentError: (Error) -> Void
|
||||
@ -37,6 +38,7 @@ class NotificationRowViewModel: ObservableObject {
|
||||
|
||||
init(
|
||||
_ notificationInfo: GroupedNotificationInfo,
|
||||
myAccountDomain: String,
|
||||
navigateToScene: @escaping (
|
||||
SceneCoordinator.Scene, SceneCoordinator.Transition
|
||||
) -> Void, presentError: @escaping (Error) -> Void
|
||||
@ -45,83 +47,70 @@ class NotificationRowViewModel: ObservableObject {
|
||||
self.identifier = .notificationGroup(id: notificationInfo.id)
|
||||
self.oldestID = notificationInfo.oldestNotificationID
|
||||
self.newestID = notificationInfo.newestNotificationID
|
||||
self.type = notificationInfo.type
|
||||
self.type = notificationInfo.groupedNotificationType
|
||||
self.iconInfo = NotificationIconInfo(
|
||||
notificationType: notificationInfo.type,
|
||||
isGrouped: notificationInfo.isGrouped,
|
||||
notificationType: notificationInfo.groupedNotificationType,
|
||||
isGrouped: notificationInfo.sourceAccounts.totalActorCount > 1,
|
||||
visibility: notificationInfo.statusViewModel?.visibility)
|
||||
self.navigateToScene = navigateToScene
|
||||
self.presentError = presentError
|
||||
self.defaultNavigation = notificationInfo.defaultNavigation
|
||||
|
||||
switch notificationInfo.type {
|
||||
switch notificationInfo.groupedNotificationType {
|
||||
|
||||
case .follow, .followRequest:
|
||||
let avatarRowAdditionalElement: RelationshipElement
|
||||
if let account = notificationInfo.primaryAuthorAccount {
|
||||
if let account = notificationInfo.sourceAccounts
|
||||
.primaryAuthorAccount
|
||||
{
|
||||
avatarRowAdditionalElement = .unfetched(
|
||||
notificationInfo.type, accountID: account.id)
|
||||
notificationInfo.groupedNotificationType)
|
||||
} else {
|
||||
avatarRowAdditionalElement = .error(nil)
|
||||
}
|
||||
avatarRow = .avatarRow(
|
||||
NotificationSourceAccounts(
|
||||
primaryAuthorAccount: notificationInfo.primaryAuthorAccount,
|
||||
avatarUrls: notificationInfo.authorAvatarUrls,
|
||||
totalActorCount: notificationInfo.authorsCount),
|
||||
notificationInfo.sourceAccounts,
|
||||
avatarRowAdditionalElement)
|
||||
if let accountName = notificationInfo.primaryAuthorAccount?
|
||||
if let accountName = notificationInfo.sourceAccounts
|
||||
.primaryAuthorAccount?
|
||||
.displayNameWithFallback
|
||||
{
|
||||
headerTextComponents = [
|
||||
.text(
|
||||
notificationInfo.type.actionSummaryLabel(
|
||||
firstAuthor: .other(named: accountName),
|
||||
totalAuthorCount: notificationInfo.authorsCount))
|
||||
notificationInfo.groupedNotificationType
|
||||
.actionSummaryLabel(notificationInfo.sourceAccounts)
|
||||
?? "")
|
||||
]
|
||||
}
|
||||
case .mention, .status:
|
||||
// TODO: eventually make this full status style, not inline
|
||||
// TODO: distinguish mentions from replies
|
||||
if let primaryAuthorAccount = notificationInfo.primaryAuthorAccount,
|
||||
let statusViewModel =
|
||||
notificationInfo.statusViewModel
|
||||
if let statusViewModel =
|
||||
notificationInfo.statusViewModel
|
||||
{
|
||||
avatarRow = .avatarRow(
|
||||
NotificationSourceAccounts(
|
||||
primaryAuthorAccount: primaryAuthorAccount,
|
||||
avatarUrls: notificationInfo.authorAvatarUrls,
|
||||
totalActorCount: notificationInfo.authorsCount),
|
||||
notificationInfo.sourceAccounts,
|
||||
.noneNeeded)
|
||||
headerTextComponents = [
|
||||
.text(
|
||||
notificationInfo.type.actionSummaryLabel(
|
||||
firstAuthor: .other(
|
||||
named: primaryAuthorAccount
|
||||
.displayNameWithFallback),
|
||||
totalAuthorCount: notificationInfo.authorsCount))
|
||||
notificationInfo.groupedNotificationType
|
||||
.actionSummaryLabel(notificationInfo.sourceAccounts)
|
||||
?? "")
|
||||
]
|
||||
contentComponents = [.status(statusViewModel)]
|
||||
} else {
|
||||
headerTextComponents = [._other("POST BY UNKNOWN ACCOUNT")]
|
||||
}
|
||||
case .reblog, .favourite:
|
||||
if let primaryAuthorAccount = notificationInfo.primaryAuthorAccount,
|
||||
let statusViewModel = notificationInfo.statusViewModel
|
||||
{
|
||||
if let statusViewModel = notificationInfo.statusViewModel {
|
||||
avatarRow = .avatarRow(
|
||||
NotificationSourceAccounts(
|
||||
primaryAuthorAccount: primaryAuthorAccount,
|
||||
avatarUrls: notificationInfo.authorAvatarUrls,
|
||||
totalActorCount: notificationInfo.authorsCount),
|
||||
notificationInfo.sourceAccounts,
|
||||
.noneNeeded)
|
||||
headerTextComponents = [
|
||||
.text(
|
||||
notificationInfo.type.actionSummaryLabel(
|
||||
firstAuthor: .other(
|
||||
named: primaryAuthorAccount
|
||||
.displayNameWithFallback),
|
||||
totalAuthorCount: notificationInfo.authorsCount))
|
||||
notificationInfo.groupedNotificationType
|
||||
.actionSummaryLabel(notificationInfo.sourceAccounts)
|
||||
?? "")
|
||||
]
|
||||
contentComponents = [.status(statusViewModel)]
|
||||
} else {
|
||||
@ -130,15 +119,14 @@ class NotificationRowViewModel: ObservableObject {
|
||||
]
|
||||
}
|
||||
case .poll, .update:
|
||||
if let author = notificationInfo.authorName,
|
||||
let statusViewModel =
|
||||
notificationInfo.statusViewModel
|
||||
if let statusViewModel =
|
||||
notificationInfo.statusViewModel
|
||||
{
|
||||
headerTextComponents = [
|
||||
.text(
|
||||
notificationInfo.type.actionSummaryLabel(
|
||||
firstAuthor: author,
|
||||
totalAuthorCount: notificationInfo.authorsCount))
|
||||
notificationInfo.groupedNotificationType
|
||||
.actionSummaryLabel(notificationInfo.sourceAccounts)
|
||||
?? "")
|
||||
]
|
||||
contentComponents = [.status(statusViewModel)]
|
||||
} else {
|
||||
@ -147,36 +135,25 @@ class NotificationRowViewModel: ObservableObject {
|
||||
]
|
||||
}
|
||||
case .adminSignUp:
|
||||
if let primaryAuthorAccount = notificationInfo.primaryAuthorAccount,
|
||||
let authorName = notificationInfo.authorName
|
||||
{
|
||||
avatarRow = .avatarRow(
|
||||
NotificationSourceAccounts(
|
||||
primaryAuthorAccount: primaryAuthorAccount,
|
||||
avatarUrls: notificationInfo.authorAvatarUrls,
|
||||
totalActorCount: notificationInfo.authorsCount),
|
||||
.noneNeeded)
|
||||
headerTextComponents = [
|
||||
.text(
|
||||
notificationInfo.type.actionSummaryLabel(
|
||||
firstAuthor: authorName,
|
||||
totalAuthorCount: notificationInfo.authorsCount))
|
||||
]
|
||||
} else {
|
||||
headerTextComponents = [._other("ADMIN_SIGNUP NOTIFICATION")]
|
||||
}
|
||||
case .adminReport:
|
||||
if let summary = notificationInfo.ruleViolationReport?.summary {
|
||||
avatarRow = .avatarRow(
|
||||
notificationInfo.sourceAccounts,
|
||||
.noneNeeded)
|
||||
headerTextComponents = [
|
||||
.text(
|
||||
notificationInfo.groupedNotificationType.actionSummaryLabel(
|
||||
notificationInfo.sourceAccounts) ?? "")
|
||||
]
|
||||
case .adminReport(let report):
|
||||
if let summary = report?.summary {
|
||||
headerTextComponents = [.text(summary)]
|
||||
}
|
||||
if let comment = notificationInfo.ruleViolationReport?
|
||||
if let comment = report?
|
||||
.displayableComment
|
||||
{
|
||||
contentComponents = [.text(comment)]
|
||||
}
|
||||
case .severedRelationships:
|
||||
if let summary = notificationInfo.relationshipSeveranceEvent?
|
||||
.summary
|
||||
case .severedRelationships(let severanceEvent):
|
||||
if let summary = severanceEvent?.summary(myDomain: myAccountDomain)
|
||||
{
|
||||
headerTextComponents = [.text(summary)]
|
||||
} else {
|
||||
@ -187,15 +164,36 @@ class NotificationRowViewModel: ObservableObject {
|
||||
]
|
||||
}
|
||||
contentComponents = [
|
||||
.hyperlinkButton("Learn more about server blocks", nil)
|
||||
] // TODO: localization and go somewhere
|
||||
case .moderationWarning:
|
||||
.hyperlinkButton(
|
||||
L10n.Scene.Notification.learnMoreAboutServerBlocks,
|
||||
notificationInfo.groupedNotificationType.learnMoreUrl(
|
||||
forDomain: myAccountDomain,
|
||||
notificationID: notificationInfo.newestNotificationID))
|
||||
]
|
||||
case .moderationWarning(let accountWarning):
|
||||
headerTextComponents = [
|
||||
.text(
|
||||
AttributedString(
|
||||
"Your account has received a moderation warning."))
|
||||
] // TODO: localization
|
||||
contentComponents = [.hyperlinkButton("Learn more", nil)] // TODO: localization and go somewhere
|
||||
.weightedText(
|
||||
(accountWarning?.action ?? .none).actionDescription,
|
||||
.regular)
|
||||
]
|
||||
|
||||
let learnMoreButton = NotificationViewComponent.hyperlinkButton(
|
||||
L10n.Scene.Notification.Warning.learnMore,
|
||||
notificationInfo.groupedNotificationType.learnMoreUrl(
|
||||
forDomain: myAccountDomain,
|
||||
notificationID: accountWarning?.id ?? notificationInfo.newestNotificationID))
|
||||
|
||||
if let accountWarningText = accountWarning?.text {
|
||||
contentComponents = [
|
||||
.weightedText(accountWarningText, .regular),
|
||||
learnMoreButton,
|
||||
]
|
||||
} else {
|
||||
contentComponents = [
|
||||
learnMoreButton
|
||||
]
|
||||
}
|
||||
|
||||
case ._other(let text):
|
||||
headerTextComponents = [
|
||||
._other("UNEXPECTED NOTIFICATION TYPE: \(text)")
|
||||
@ -226,7 +224,10 @@ class NotificationRowViewModel: ObservableObject {
|
||||
) {
|
||||
switch type {
|
||||
case .follow, .followRequest:
|
||||
guard let accountID = sourceAccounts.firstAccountID, let accountIsLocked = sourceAccounts.primaryAuthorAccount?.locked else { return }
|
||||
guard let accountID = sourceAccounts.firstAccountID,
|
||||
let accountIsLocked = sourceAccounts.primaryAuthorAccount?
|
||||
.locked
|
||||
else { return }
|
||||
avatarRow = .avatarRow(sourceAccounts, .fetching)
|
||||
|
||||
Task { @MainActor in
|
||||
@ -240,9 +241,11 @@ class NotificationRowViewModel: ObservableObject {
|
||||
case (.follow, true):
|
||||
element = .iFollowThem(theyFollowMe: true)
|
||||
case (.follow, false):
|
||||
element = .iDoNotFollowThem(theirAccountIsLocked: accountIsLocked)
|
||||
element = .iDoNotFollowThem(
|
||||
theirAccountIsLocked: accountIsLocked)
|
||||
case (.followRequest, _):
|
||||
element = .theyHaveRequestedToFollowMe(iFollowThem: relationship.following)
|
||||
element = .theyHaveRequestedToFollowMe(
|
||||
iFollowThem: relationship.following)
|
||||
default:
|
||||
element = .noneNeeded
|
||||
}
|
||||
@ -311,8 +314,11 @@ extension NotificationRowViewModel {
|
||||
switch avatarRow {
|
||||
case .avatarRow(let accountInfo, let relationshipElement):
|
||||
switch relationshipElement {
|
||||
case .iDoNotFollowThem, .iFollowThem, .iHaveRequestedToFollowThem:
|
||||
await doFollowAction(relationshipElement.followAction, notificationSourceAccounts: accountInfo)
|
||||
case .iDoNotFollowThem, .iFollowThem,
|
||||
.iHaveRequestedToFollowThem:
|
||||
await doFollowAction(
|
||||
relationshipElement.followAction,
|
||||
notificationSourceAccounts: accountInfo)
|
||||
case .theyHaveRequestedToFollowMe:
|
||||
await doAnswerFollowRequest(accountInfo, accept: accept)
|
||||
default:
|
||||
@ -325,8 +331,13 @@ extension NotificationRowViewModel {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func doFollowAction(_ action: RelationshipElement.FollowAction, notificationSourceAccounts: NotificationSourceAccounts) async {
|
||||
guard let accountID = notificationSourceAccounts.firstAccountID, let theirAccountIsLocked = notificationSourceAccounts.primaryAuthorAccount?.locked,
|
||||
private func doFollowAction(
|
||||
_ action: RelationshipElement.FollowAction,
|
||||
notificationSourceAccounts: NotificationSourceAccounts
|
||||
) async {
|
||||
guard let accountID = notificationSourceAccounts.firstAccountID,
|
||||
let theirAccountIsLocked = notificationSourceAccounts
|
||||
.primaryAuthorAccount?.locked,
|
||||
let authBox = AuthenticationServiceProvider.shared.currentActiveUser
|
||||
.value
|
||||
else { return }
|
||||
@ -340,16 +351,20 @@ extension NotificationRowViewModel {
|
||||
response = try await APIService.shared.follow(
|
||||
accountID, authenticationBox: authBox)
|
||||
case .unfollow:
|
||||
response = try await APIService.shared.unfollow(accountID, authenticationBox: authBox)
|
||||
response = try await APIService.shared.unfollow(
|
||||
accountID, authenticationBox: authBox)
|
||||
case .noAction:
|
||||
throw AppError.unexpected("action attempted for relationship element that has no action")
|
||||
throw AppError.unexpected(
|
||||
"action attempted for relationship element that has no action"
|
||||
)
|
||||
}
|
||||
if response.following {
|
||||
updatedElement = .iFollowThem(theyFollowMe: response.followedBy)
|
||||
} else if response.requested {
|
||||
updatedElement = .iHaveRequestedToFollowThem
|
||||
} else {
|
||||
updatedElement = .iDoNotFollowThem(theirAccountIsLocked: theirAccountIsLocked)
|
||||
updatedElement = .iDoNotFollowThem(
|
||||
theirAccountIsLocked: theirAccountIsLocked)
|
||||
}
|
||||
avatarRow = .avatarRow(notificationSourceAccounts, updatedElement)
|
||||
} catch {
|
||||
@ -380,7 +395,8 @@ extension NotificationRowViewModel {
|
||||
return
|
||||
}
|
||||
self.avatarRow = .avatarRow(
|
||||
accountInfo, .iHaveAnsweredTheirRequestToFollowMe(didAccept: accept))
|
||||
accountInfo,
|
||||
.iHaveAnsweredTheirRequestToFollowMe(didAccept: accept))
|
||||
} catch {
|
||||
presentError(error)
|
||||
self.avatarRow = startingAvatarRow
|
||||
@ -428,27 +444,22 @@ extension NotificationRowViewModel {
|
||||
?? partialAccounts?[accountID]?.avatarURL
|
||||
}
|
||||
|
||||
let authorName: Mastodon.Entity.NotificationType.AuthorName?
|
||||
if primaryAccount?.id == myAccountID {
|
||||
authorName = .me
|
||||
} else if let name = primaryAccount?.displayNameWithFallback {
|
||||
authorName = .other(named: name)
|
||||
} else {
|
||||
authorName = nil
|
||||
}
|
||||
let sourceAccounts = NotificationSourceAccounts(
|
||||
myAccountID: myAccountID, primaryAuthorAccount: primaryAccount,
|
||||
avatarUrls: avatarUrls,
|
||||
totalActorCount: group.notificationsCount)
|
||||
|
||||
let status = group.statusID == nil ? nil : statuses[group.statusID!]
|
||||
|
||||
let type = GroupedNotificationType(
|
||||
group, sourceAccounts: sourceAccounts, status: status)
|
||||
|
||||
let info = GroupedNotificationInfo(
|
||||
id: group.id,
|
||||
oldestNotificationID: group.oldestNotificationID,
|
||||
newestNotificationID: group.newestNotificationID,
|
||||
type: group.type,
|
||||
authorsCount: group.authorsCount,
|
||||
notificationsCount: group.notificationsCount,
|
||||
primaryAuthorAccount: primaryAccount,
|
||||
authorName: authorName,
|
||||
authorAvatarUrls: avatarUrls,
|
||||
oldestNotificationID: group.pageNewestID ?? "",
|
||||
newestNotificationID: group.pageOldestID ?? "",
|
||||
groupedNotificationType: type,
|
||||
sourceAccounts: sourceAccounts,
|
||||
statusViewModel: status?.viewModel(
|
||||
myDomain: myAccountDomain,
|
||||
navigateToStatus: {
|
||||
@ -470,12 +481,10 @@ extension NotificationRowViewModel {
|
||||
false))))), .show)
|
||||
}
|
||||
}),
|
||||
ruleViolationReport: group.ruleViolationReport,
|
||||
relationshipSeveranceEvent: group.relationshipSeveranceEvent,
|
||||
defaultNavigation: {
|
||||
guard
|
||||
let navigation = defaultNavigation(
|
||||
group.type, isGrouped: group.isGrouped,
|
||||
group.type, isGrouped: group.notificationsCount > 1,
|
||||
primaryAccount: primaryAccount)
|
||||
else { return }
|
||||
Task {
|
||||
@ -487,7 +496,8 @@ extension NotificationRowViewModel {
|
||||
)
|
||||
|
||||
return NotificationRowViewModel(
|
||||
info, navigateToScene: navigateToScene,
|
||||
info, myAccountDomain: myAccountDomain,
|
||||
navigateToScene: navigateToScene,
|
||||
presentError: presentError)
|
||||
}
|
||||
}
|
||||
@ -502,40 +512,42 @@ extension NotificationRowViewModel {
|
||||
) -> [NotificationRowViewModel] {
|
||||
|
||||
return notifications.map { notification in
|
||||
let sourceAccounts = NotificationSourceAccounts(
|
||||
myAccountID: myAccountID,
|
||||
primaryAuthorAccount: notification.account,
|
||||
avatarUrls: notification.authorAvatarUrls, totalActorCount: 1)
|
||||
|
||||
let statusViewModel = notification.status?.viewModel(
|
||||
myDomain: myAccountDomain,
|
||||
navigateToStatus: {
|
||||
Task {
|
||||
guard
|
||||
let authBox =
|
||||
await AuthenticationServiceProvider.shared
|
||||
.currentActiveUser.value,
|
||||
let status = notification.status
|
||||
else { return }
|
||||
await navigateToScene(
|
||||
.thread(
|
||||
viewModel: ThreadViewModel(
|
||||
authenticationBox: authBox,
|
||||
optionalRoot: .root(
|
||||
context: .init(
|
||||
status: MastodonStatus(
|
||||
entity: status,
|
||||
showDespiteContentWarning:
|
||||
false))))), .show)
|
||||
}
|
||||
})
|
||||
|
||||
let info = GroupedNotificationInfo(
|
||||
id: notification.id,
|
||||
oldestNotificationID: notification.id,
|
||||
newestNotificationID: notification.id,
|
||||
type: notification.type,
|
||||
authorsCount: notification.authorsCount,
|
||||
notificationsCount: 1,
|
||||
primaryAuthorAccount: notification.account,
|
||||
authorName: notification.authorName,
|
||||
authorAvatarUrls: notification.authorAvatarUrls,
|
||||
statusViewModel: notification.status?.viewModel(
|
||||
myDomain: myAccountDomain,
|
||||
navigateToStatus: {
|
||||
Task {
|
||||
guard
|
||||
let authBox =
|
||||
await AuthenticationServiceProvider.shared
|
||||
.currentActiveUser.value,
|
||||
let status = notification.status
|
||||
else { return }
|
||||
await navigateToScene(
|
||||
.thread(
|
||||
viewModel: ThreadViewModel(
|
||||
authenticationBox: authBox,
|
||||
optionalRoot: .root(
|
||||
context: .init(
|
||||
status: MastodonStatus(
|
||||
entity: status,
|
||||
showDespiteContentWarning:
|
||||
false))))), .show)
|
||||
}
|
||||
}),
|
||||
ruleViolationReport: notification.ruleViolationReport,
|
||||
relationshipSeveranceEvent: notification.relationshipSeveranceEvent,
|
||||
groupedNotificationType: GroupedNotificationType(
|
||||
notification, sourceAccounts: sourceAccounts),
|
||||
sourceAccounts: sourceAccounts,
|
||||
statusViewModel: statusViewModel,
|
||||
defaultNavigation: {
|
||||
guard
|
||||
let navigation = defaultNavigation(
|
||||
@ -551,7 +563,8 @@ extension NotificationRowViewModel {
|
||||
)
|
||||
|
||||
return NotificationRowViewModel(
|
||||
info, navigateToScene: navigateToScene,
|
||||
info, myAccountDomain: myAccountDomain,
|
||||
navigateToScene: navigateToScene,
|
||||
presentError: presentError)
|
||||
}
|
||||
}
|
||||
@ -615,3 +628,144 @@ extension NotificationRowViewModel {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupedNotificationType {
|
||||
init(
|
||||
_ notification: Mastodon.Entity.Notification,
|
||||
sourceAccounts: NotificationSourceAccounts
|
||||
) {
|
||||
switch notification.typeFromServer {
|
||||
case .follow:
|
||||
self = .follow(from: sourceAccounts)
|
||||
case .followRequest:
|
||||
if let account = sourceAccounts.primaryAuthorAccount {
|
||||
self = .followRequest(from: account)
|
||||
} else {
|
||||
self = ._other("Follow request from unknown account")
|
||||
}
|
||||
case .mention:
|
||||
self = .mention(notification.status)
|
||||
case .reblog:
|
||||
self = .mention(notification.status)
|
||||
case .favourite:
|
||||
self = .favourite(notification.status)
|
||||
case .poll:
|
||||
self = .poll(notification.status)
|
||||
case .status:
|
||||
self = .status(notification.status)
|
||||
case .update:
|
||||
self = .update(notification.status)
|
||||
case .adminSignUp:
|
||||
self = .adminSignUp
|
||||
case .adminReport:
|
||||
self = .adminReport(notification.ruleViolationReport)
|
||||
case .severedRelationships:
|
||||
self = .severedRelationships(
|
||||
notification.relationshipSeveranceEvent)
|
||||
case .moderationWarning:
|
||||
self = .moderationWarning(notification.accountWarning)
|
||||
case ._other(let string):
|
||||
self = ._other(string)
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
_ notificationGroup: Mastodon.Entity.NotificationGroup,
|
||||
sourceAccounts: NotificationSourceAccounts,
|
||||
status: Mastodon.Entity.Status?
|
||||
) {
|
||||
switch notificationGroup.type {
|
||||
case .follow:
|
||||
self = .follow(from: sourceAccounts)
|
||||
case .followRequest:
|
||||
if let account = sourceAccounts.primaryAuthorAccount {
|
||||
self = .followRequest(from: account)
|
||||
} else {
|
||||
self = ._other("Follow request from unknown account")
|
||||
}
|
||||
case .mention:
|
||||
self = .mention(status)
|
||||
case .reblog:
|
||||
self = .mention(status)
|
||||
case .favourite:
|
||||
self = .favourite(status)
|
||||
case .poll:
|
||||
self = .poll(status)
|
||||
case .status:
|
||||
self = .status(status)
|
||||
case .update:
|
||||
self = .update(status)
|
||||
case .adminSignUp:
|
||||
self = .adminSignUp
|
||||
case .adminReport:
|
||||
self = .adminReport(notificationGroup.ruleViolationReport)
|
||||
case .severedRelationships:
|
||||
self = .severedRelationships(
|
||||
notificationGroup.relationshipSeveranceEvent)
|
||||
case .moderationWarning:
|
||||
self = .moderationWarning(notificationGroup.accountWarning)
|
||||
case ._other(let string):
|
||||
self = ._other(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationSourceAccounts {
|
||||
var authorsDescription: String? {
|
||||
switch authorName {
|
||||
case .me, .none:
|
||||
return nil
|
||||
case .other(let name):
|
||||
if totalActorCount > 1 {
|
||||
return "\(name) and \(totalActorCount - 1) others"
|
||||
} else {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupedNotificationType {
|
||||
func learnMoreUrl(forDomain domain: String, notificationID: String) -> URL?
|
||||
{
|
||||
let trailingPathComponents: [String]
|
||||
switch self {
|
||||
case .severedRelationships:
|
||||
trailingPathComponents = ["severed_relationships"]
|
||||
case .moderationWarning:
|
||||
trailingPathComponents = [
|
||||
"disputes",
|
||||
"strikes",
|
||||
notificationID,
|
||||
]
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
var url = URL(string: "https://" + domain)
|
||||
for component in trailingPathComponents {
|
||||
url?.append(component: component)
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.AccountWarning.Action {
|
||||
var actionDescription: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return L10n.Scene.Notification.Warning.none
|
||||
case .disable:
|
||||
return L10n.Scene.Notification.Warning.disable
|
||||
case .markStatusesAsSensitive:
|
||||
return L10n.Scene.Notification.Warning.markStatusesAsSensitive
|
||||
case .deleteStatuses:
|
||||
return L10n.Scene.Notification.Warning.deleteStatuses
|
||||
case .sensitive:
|
||||
return L10n.Scene.Notification.Warning.sensitive
|
||||
case .silence:
|
||||
return L10n.Scene.Notification.Warning.silence
|
||||
case .suspend:
|
||||
return L10n.Scene.Notification.Warning.suspend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ class TimelinePostViewModel {
|
||||
init(feedItemIdentifier: MastodonFeedItemIdentifier, includePadding: Bool) {
|
||||
self.includePadding = includePadding
|
||||
self.feedItemIdentifier = feedItemIdentifier
|
||||
if let notification = MastodonFeedItemCacheManager.shared.cachedItem(feedItemIdentifier) as? NotificationInfo {
|
||||
boostingAccountName = notification.type == .reblog ? notification.authorName?.string : nil
|
||||
if let notification = MastodonFeedItemCacheManager.shared.cachedItem(feedItemIdentifier) as? Mastodon.Entity.Notification {
|
||||
boostingAccountName = notification.type == .reblog ? notification.primaryAuthorAccount?.displayNameWithFallback : nil
|
||||
} else {
|
||||
boostingAccountName = nil
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user