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) }
- }
-}