diff --git a/IceCubesApp/Resources/Localization/Plurals/nl.lproj/Localizable.stringsdict b/IceCubesApp/Resources/Localization/Plurals/nl.lproj/Localizable.stringsdict index a7c1fef3..3ef94320 100644 --- a/IceCubesApp/Resources/Localization/Plurals/nl.lproj/Localizable.stringsdict +++ b/IceCubesApp/Resources/Localization/Plurals/nl.lproj/Localizable.stringsdict @@ -18,6 +18,7 @@ %lld nieuwe posts + notifications-others-count %lld NSStringLocalizedFormatKey @@ -34,5 +35,73 @@ en %lld anderen + notifications.label.mention %lld + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + one + heeft jou vermeld + other + hebben jou vermeld + + + + notifications.label.follow %lld + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + one + volgt jou + other + volgen jou + + + + notifications.label.reblog %lld + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + one + boostte + other + boostten + + + + notifications.label.favorite %lld + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + one + markeerde jouw bericht als favoriet + other + markeerden jouw bericht als favoriet + + + diff --git a/IceCubesApp/Resources/Localization/Plurals/zh-Hans.lproj/Localizable.stringsdict b/IceCubesApp/Resources/Localization/Plurals/zh-Hans.lproj/Localizable.stringsdict index ffefcbc0..3fea4ed0 100644 --- a/IceCubesApp/Resources/Localization/Plurals/zh-Hans.lproj/Localizable.stringsdict +++ b/IceCubesApp/Resources/Localization/Plurals/zh-Hans.lproj/Localizable.stringsdict @@ -13,26 +13,26 @@ NSStringFormatValueTypeKey lld one - %lld 个新嘟文 + %lld 条新嘟文 other - %lld 个新嘟文 + %lld 条新嘟文 notifications-others-count %lld - - NSStringLocalizedFormatKey - %#@noficationsOthersCount@ - noficationsOthersCount - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - lld - one - 和其它 %lld 个用户 - other - 和其它 %lld 个用户 - - + + NSStringLocalizedFormatKey + %#@noficationsOthersCount@ + noficationsOthersCount + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lld + one + 和其他 %lld 个用户 + other + 和其他 %lld 个用户 + + diff --git a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings index 7c24a0b8..b82f3987 100644 --- a/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/nl.lproj/Localizable.strings @@ -1,7 +1,7 @@ // MARK: Common strings "action.cancel" = "Annuleer"; "action.delete" = "Verwijder"; -"action.save" = "Opslaan"; +"action.save" = "Bewaar"; "action.done" = "Gereed"; "action.retry" = "Opnieuw"; "action.view.error" = "Bekijk fout"; @@ -125,10 +125,10 @@ "settings.push.duplicate.title" = "Dubbele meldingen"; "settings.push.duplicate.footer" = "Ontvang je dubbele meldingen? Gebruik deze magische knop om dit probleem te verhelpen"; "settings.push.duplicate.button.fix" = "🪄 Los op"; -"settings.other.autoplay-video" = "Auto Play Videos"; -"settings.display.font" = "Timeline Font"; -"settings.display.font.system" = "System"; -"settings.display.font.custom" = "Custom"; +"settings.other.autoplay-video" = "Speel video’s automatisch af"; +"settings.display.font" = "Tijdlijnlettertype"; +"settings.display.font.system" = "Systeem"; +"settings.display.font.custom" = "Aangepast"; // MARK: Tabs "tab.explore" = "Ontdekken"; @@ -177,8 +177,8 @@ "account.edit.account-settings.private" = "Privé"; "account.edit.account-settings.section-title" = "Accountinstellingen"; "account.edit.display-name" = "Weergavenaam"; -"account.edit.error.save.message" = "Er heeft zich een fout voorgedaan tijdens het opslaan van je profiel. Probeer het nogmaals"; -"account.edit.error.save.title" = "Fout tijdens het opslaan van je profiel"; +"account.edit.error.save.message" = "Er heeft zich een fout voorgedaan tijdens het bewaren van je profiel. Probeer het nogmaals"; +"account.edit.error.save.title" = "Fout tijdens het bewaren van je profiel"; "account.edit.navigation-title" = "Profiel bewerken"; "account.edit.post-settings.privacy" = "Standaardprivacy"; "account.edit.post-settings.section-title" = "Postinstellingen"; @@ -260,12 +260,8 @@ "notifications.empty.title" = "Geen notificaties"; "notifications.error.message" = "Er heeft zich een fout voorgedaan tijdens het laden van je notificaties. Probeer het nogmaals."; "notifications.error.title" = "Er heeft zich een fout voorgedaan"; -"notifications.label.favorite %lld" = "heeft jouw bericht als favoriet gemarkeerd"; -"notifications.label.follow %lld" = "volgt jou"; "notifications.label.follow-request" = "wil je volgen"; -"notifications.label.mention %lld" = "heeft jou vermeld"; "notifications.label.poll" = "poll beëindigd"; -"notifications.label.reblog %lld" = "boostte"; "notifications.label.status" = "nieuwe status"; "notifications.label.update" = "wijzigde een post"; "notifications.menu-title.favorite" = "Favoriet"; @@ -289,7 +285,7 @@ // MARK: Package: Status "status.action.translate" = "Vertaal"; -"status.action.translate-from-%@" = "Vertaal uit %@"; +"status.action.translate-from-%@" = "Vertaal uit het %@"; "status.action.translated-label" = "Vertaald met behulp van DeepL.com"; "status.action.bookmark" = "Voeg bladwijzer toe"; "status.action.boost" = "Boosten"; @@ -310,8 +306,8 @@ "status.action.unfavorite" = "Verwijder favoriet"; "status.action.unpin" = "Maak los"; "status.action.view-in-browser" = "Open in browser"; -"status.draft.delete" = "Concept verwijderen"; -"status.draft.save" = "Concept opslaan"; +"status.draft.delete" = "Verwijder concept"; +"status.draft.save" = "Bewaar concept"; "status.editor.ai-prompt.correct" = "Corrigeer tekst"; "status.editor.ai-prompt.emphasize" = "Benadruk tekst"; "status.editor.ai-prompt.fit" = "Kort tekst in"; diff --git a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings index 30341793..41f4c70d 100644 --- a/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings +++ b/IceCubesApp/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -108,7 +108,7 @@ "settings.timeline.add" = "添加远程时间线"; "settings.title" = "设置"; "settings.rate" = "给 Ice Cubes 评分"; -"settings.section.other" = "其它"; +"settings.section.other" = "其他"; "settings.other.hide-openai" = "启用写作助手 🤖"; "settings.other.social-keyboard" = "启用社交键盘"; @@ -129,9 +129,9 @@ "settings.push.duplicate.button.fix" = "🪄 修复"; "settings.other.autoplay-video" = "自动播放视频"; -"settings.display.font" = "Timeline Font"; -"settings.display.font.system" = "System"; -"settings.display.font.custom" = "Custom"; +"settings.display.font" = "时间线字体"; +"settings.display.font.system" = "系统"; +"settings.display.font.custom" = "自定义"; // MARK: Tabs "tab.explore" = "探索"; @@ -229,7 +229,7 @@ "explore.search.message-%@" = "在此界面上,你可以搜索 %@ 上的任何信息"; "explore.search.prompt" = "搜索用户、嘟文或标签"; "explore.search.title" = "搜索你的服务器"; -"explore.search.empty.message" = "搜索无结果,请尝试其它查询"; +"explore.search.empty.message" = "搜索无结果,请尝试其他查询"; "explore.search.empty.title" = "无结果"; "explore.section.posts" = "嘟文"; "explore.section.suggested-users" = "推荐的用户"; @@ -270,7 +270,7 @@ "notifications.label.poll" = "投票结束"; "notifications.label.reblog %lld" = "已转发"; "notifications.label.status" = "嘟嘟了一个状态"; -"notifications.label.update" = "编辑了一个嘟文"; +"notifications.label.update" = "编辑了一条嘟文"; "notifications.menu-title.favorite" = "收藏"; "notifications.menu-title.follow" = "关注"; "notifications.menu-title.follow-request" = "关注申请"; @@ -293,7 +293,7 @@ // MARK: Package: Status "status.action.translate" = "翻译"; "status.action.translate-from-%@" = "翻译 %@"; -"status.action.translated-label" = "使用 DeepL.com 翻译"; +"status.action.translated-label" = "由 DeepL.com 翻译"; "status.action.bookmark" = "书签"; "status.action.boost" = "转发"; "status.action.copy-text" = "拷贝文本"; @@ -329,10 +329,10 @@ "status.editor.mode.edit" = "正在编辑你的嘟文"; "status.editor.mode.new" = "新嘟文"; "status.editor.mode.quote-%@" = "%@ 的引用"; -"status.editor.mode.reply-%@" = "正在回复 %@"; +"status.editor.mode.reply-%@" = "回复 %@"; "status.editor.restore-previous" = "撤销更改"; "status.editor.spoiler" = "剧透警告"; -"status.editor.text.placeholder" = "在想些什么?"; +"status.editor.text.placeholder" = "在想些什么呢?"; "status.editor.visibility" = "嘟文可见性"; "status.error.loading.message" = "加载嘟文时发生错误,请重试。"; "status.error.message" = "嘟文的上下文出现了错误,请重试。"; diff --git a/Packages/Models/Sources/Models/ConsolidatedNotification.swift b/Packages/Models/Sources/Models/ConsolidatedNotification.swift new file mode 100644 index 00000000..78ac0357 --- /dev/null +++ b/Packages/Models/Sources/Models/ConsolidatedNotification.swift @@ -0,0 +1,43 @@ +// +// ConsolidatedNotification.swift +// +// +// Created by Jérôme Danthinne on 31/01/2023. +// + +import Foundation + +public struct ConsolidatedNotification: Identifiable { + public let notifications: [Notification] + public let type: Notification.NotificationType + public let createdAt: ServerDate + public let accounts: [Account] + public let status: Status? + + public var id: String? { notifications.first?.id } + + public init(notifications: [Notification], + type: Notification.NotificationType, + createdAt: ServerDate, + accounts: [Account], + status: Status?) + { + self.notifications = notifications + self.type = type + self.createdAt = createdAt + self.accounts = accounts + self.status = status + } + + public static func placeholder() -> ConsolidatedNotification { + .init(notifications: [Notification.placeholder()], + type: .favourite, + createdAt: "2022-12-16T10:20:54.000Z", + accounts: [.placeholder()], + status: .placeholder()) + } + + public static func placeholders() -> [ConsolidatedNotification] { + [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] + } +} diff --git a/Packages/Models/Sources/Models/Notification.swift b/Packages/Models/Sources/Models/Notification.swift index 872fe1a3..a82c119a 100644 --- a/Packages/Models/Sources/Models/Notification.swift +++ b/Packages/Models/Sources/Models/Notification.swift @@ -14,4 +14,12 @@ public struct Notification: Decodable, Identifiable { public var supportedType: NotificationType? { .init(rawValue: type) } + + public static func placeholder() -> Notification { + .init(id: UUID().uuidString, + type: NotificationType.favourite.rawValue, + createdAt: "2022-12-16T10:20:54.000Z", + account: .placeholder(), + status: .placeholder()) + } } diff --git a/Packages/Notifications/Sources/Notifications/ConsolidatedNotificationExt.swift b/Packages/Notifications/Sources/Notifications/ConsolidatedNotificationExt.swift new file mode 100644 index 00000000..fa91668c --- /dev/null +++ b/Packages/Notifications/Sources/Notifications/ConsolidatedNotificationExt.swift @@ -0,0 +1,18 @@ +// +// ConsolidatedNotificationExt.swift +// +// +// Created by Jérôme Danthinne on 31/01/2023. +// + +import Models + +extension ConsolidatedNotification { + var notificationIds: [String] { notifications.map(\.id) } +} + +extension Array where Element == ConsolidatedNotification { + var notificationCount: Int { + reduce(0) { $0 + ($1.accounts.isEmpty ? 1 : $1.accounts.count) } + } +} diff --git a/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift b/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift new file mode 100644 index 00000000..3a2c343e --- /dev/null +++ b/Packages/Notifications/Sources/Notifications/Notification+Consolidated.swift @@ -0,0 +1,29 @@ +// +// Notification+Consolidated.swift +// +// +// Created by Jérôme Danthinne on 31/01/2023. +// + +import Models + +extension Array where Element == Notification { + func consolidated(selectedType: Notification.NotificationType?) -> [ConsolidatedNotification] { + Dictionary(grouping: self) { $0.consolidationId(selectedType: selectedType) } + .values + .compactMap { notifications in + guard let notification = notifications.first, + let supportedType = notification.supportedType + else { return nil } + + return ConsolidatedNotification(notifications: notifications, + type: supportedType, + createdAt: notification.createdAt, + accounts: notifications.map(\.account), + status: notification.status) + } + .sorted { + $0.createdAt > $1.createdAt + } + } +} diff --git a/Packages/Notifications/Sources/Notifications/NotificationExt.swift b/Packages/Notifications/Sources/Notifications/NotificationExt.swift new file mode 100644 index 00000000..a9e70d22 --- /dev/null +++ b/Packages/Notifications/Sources/Notifications/NotificationExt.swift @@ -0,0 +1,31 @@ +// +// NotificationExt.swift +// +// +// Created by Jérôme Danthinne on 31/01/2023. +// + +import Models + +extension Notification { + func consolidationId(selectedType: Models.Notification.NotificationType?) -> String? { + guard let supportedType else { return nil } + + switch supportedType { + case .follow where selectedType != .follow: + // Always group followers, so use the type to group + return supportedType.rawValue + case .reblog, .favourite: + // Group boosts and favourites by status, so use the type + the related status id + return "\(supportedType.rawValue)-\(status?.id ?? "")" + default: + // Never group remaining ones, so use the notification id itself + return id + } + } + + func isConsolidable(selectedType: Models.Notification.NotificationType?) -> Bool { + // Notification is consolidable onlt if the consolidation id is not the notication id (unique) itself + consolidationId(selectedType: selectedType) != id + } +} diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index ea616d91..6c33a8d1 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -23,7 +23,6 @@ class NotificationsViewModel: ObservableObject { var client: Client? { didSet { if oldValue != client { - notifications = [] consolidatedNotifications = [] } } @@ -33,7 +32,6 @@ class NotificationsViewModel: ObservableObject { @Published var selectedType: Models.Notification.NotificationType? { didSet { if oldValue != selectedType { - notifications = [] consolidatedNotifications = [] Task { await fetchNotifications() @@ -51,7 +49,6 @@ class NotificationsViewModel: ObservableObject { return nil } - private var notifications: [Models.Notification] = [] private var consolidatedNotifications: [ConsolidatedNotification] = [] func fetchNotifications() async { @@ -64,7 +61,6 @@ class NotificationsViewModel: ObservableObject { try await client.get(endpoint: Notifications.notifications(sinceId: nil, maxId: nil, types: queryTypes)) - self.notifications = notifications consolidatedNotifications = notifications.consolidated(selectedType: selectedType) nextPageState = notifications.count < 15 ? .none : .hasNextPage } else if let first = consolidatedNotifications.first { @@ -76,7 +72,6 @@ class NotificationsViewModel: ObservableObject { newNotifications = newNotifications.filter { notification in !consolidatedNotifications.contains(where: { $0.id == notification.id }) } - notifications.append(contentsOf: newNotifications) consolidatedNotifications.insert( contentsOf: newNotifications.consolidated(selectedType: selectedType), at: 0 @@ -101,7 +96,6 @@ class NotificationsViewModel: ObservableObject { maxId: lastId, types: queryTypes)) consolidatedNotifications.append(contentsOf: newNotifications.consolidated(selectedType: selectedType)) - notifications.append(contentsOf: newNotifications) state = .display(notifications: consolidatedNotifications, nextPageState: newNotifications.count < 15 ? .none : .hasNextPage) } catch { state = .error(error: error) @@ -117,16 +111,29 @@ class NotificationsViewModel: ObservableObject { func handleEvent(event: any StreamEvent) { Task { + // Check if the event is a notification, + // if it is not already in the list, + // and if it can be shown (no selected type or the same as the received notification type) if let event = event as? StreamEventNotification, - !consolidatedNotifications.contains(where: { $0.id == event.notification.id }) + !consolidatedNotifications.flatMap(\.notificationIds).contains(event.notification.id), + selectedType == nil || selectedType?.rawValue == event.notification.type { - if let selectedType, event.notification.type == selectedType.rawValue { - notifications.insert(event.notification, at: 0) - consolidatedNotifications = notifications.consolidated(selectedType: selectedType) - } else if selectedType == nil { - notifications.insert(event.notification, at: 0) - consolidatedNotifications = notifications.consolidated(selectedType: selectedType) + if event.notification.isConsolidable(selectedType: selectedType) { + // If the notification type can be consolidated, try to consolidate with the latest row + let latestConsolidatedNotification = consolidatedNotifications.removeFirst() + consolidatedNotifications.insert( + contentsOf: ([event.notification] + latestConsolidatedNotification.notifications) + .consolidated(selectedType: selectedType), + at: 0 + ) + } else { + // Otherwise, just insert the new notification + consolidatedNotifications.insert( + contentsOf: [event.notification].consolidated(selectedType: selectedType), + at: 0 + ) } + withAnimation { state = .display(notifications: consolidatedNotifications, nextPageState: .hasNextPage) } @@ -134,66 +141,3 @@ class NotificationsViewModel: ObservableObject { } } } - -struct ConsolidatedNotification: Identifiable { - let notificationIds: [String] - let type: Models.Notification.NotificationType - let createdAt: ServerDate - let accounts: [Account] - let status: Status? - - var id: String? { notificationIds.first } - - static func placeholder() -> ConsolidatedNotification { - .init(notificationIds: [UUID().uuidString], - type: .favourite, - createdAt: "2022-12-16T10:20:54.000Z", - accounts: [.placeholder()], - status: .placeholder()) - } - - static func placeholders() -> [ConsolidatedNotification] { - [.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()] - } -} - -extension Array where Element == Models.Notification { - func consolidated(selectedType: Models.Notification.NotificationType?) -> [ConsolidatedNotification] { - Dictionary(grouping: self) { notification -> String? in - guard let supportedType = notification.supportedType else { return nil } - - switch supportedType { - case .follow where selectedType != .follow: - // Always group followers - return supportedType.rawValue - case .reblog, .favourite: - // Group boosts and favourites by status - return "\(supportedType.rawValue)-\(notification.status?.id ?? "")" - default: - // Never group remaining ones - return notification.id - } - } - .values - .compactMap { notifications in - guard let notification = notifications.first, - let supportedType = notification.supportedType - else { return nil } - - return ConsolidatedNotification(notificationIds: notifications.map(\.id), - type: supportedType, - createdAt: notification.createdAt, - accounts: notifications.map(\.account), - status: notification.status) - } - .sorted { - $0.createdAt > $1.createdAt - } - } -} - -extension Array where Element == ConsolidatedNotification { - var notificationCount: Int { - reduce(0) { $0 + ($1.accounts.isEmpty ? 1 : $1.accounts.count) } - } -}