mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 16:31:00 +00:00
* Group favorite and boost notifications * Group notifications per page, not globally
This commit is contained in:
parent
1824721a57
commit
bec9ab8792
12 changed files with 302 additions and 99 deletions
|
@ -18,5 +18,21 @@
|
||||||
<string>%lld neue Posts</string>
|
<string>%lld neue Posts</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -2,21 +2,37 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>timeline-new-posts %lld</key>
|
<key>timeline-new-posts %lld</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
<string>%#@timelineNewPosts@</string>
|
<string>%#@timelineNewPosts@</string>
|
||||||
<key>timelineNewPosts</key>
|
<key>timelineNewPosts</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringFormatSpecTypeKey</key>
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
<string>NSStringPluralRuleType</string>
|
<string>NSStringPluralRuleType</string>
|
||||||
<key>NSStringFormatValueTypeKey</key>
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
<string>lld</string>
|
<string>lld</string>
|
||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%lld new post</string>
|
<string>%lld new post</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%lld new posts</string>
|
<string>%lld new posts</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -18,5 +18,21 @@
|
||||||
<string>%lld nuevas publicaciones</string>
|
<string>%lld nuevas publicaciones</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -18,5 +18,21 @@
|
||||||
<string>%lld nuovi post</string>
|
<string>%lld nuovi post</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -18,5 +18,21 @@
|
||||||
<string>%lld 新しい投稿</string>
|
<string>%lld 新しい投稿</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -18,5 +18,21 @@
|
||||||
<string>%lld nieuwe posts</string>
|
<string>%lld nieuwe posts</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -18,5 +18,21 @@
|
||||||
<string>%lld yeni gönderiler</string>
|
<string>%lld yeni gönderiler</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -18,5 +18,21 @@
|
||||||
<string>%lld 个新嘟文</string>
|
<string>%lld 个新嘟文</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notifications-others-count %lld</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@noficationsOthersCount@</string>
|
||||||
|
<key>noficationsOthersCount</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>lld</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string> and %lld other </string>
|
||||||
|
<key>other</key>
|
||||||
|
<string> and %lld others </string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -14,16 +14,4 @@ public struct Notification: Decodable, Identifiable {
|
||||||
public var supportedType: NotificationType? {
|
public var supportedType: NotificationType? {
|
||||||
.init(rawValue: type)
|
.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())
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func placeholders() -> [Notification] {
|
|
||||||
[.placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder(), .placeholder()]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,56 +11,81 @@ struct NotificationRowView: View {
|
||||||
@EnvironmentObject private var routerPath: RouterPath
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
|
|
||||||
let notification: Models.Notification
|
let notification: ConsolidatedNotification
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let type = notification.supportedType {
|
HStack(alignment: .top, spacing: 8) {
|
||||||
HStack(alignment: .top, spacing: 8) {
|
if notification.accounts.count == 1 {
|
||||||
makeAvatarView(type: type)
|
makeAvatarView(type: notification.type)
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
} else {
|
||||||
makeMainLabel(type: type)
|
makeNotificationIconView(type: notification.type)
|
||||||
makeContent(type: type)
|
.frame(width: AvatarView.Size.status.size.width,
|
||||||
if type == .follow_request,
|
height: AvatarView.Size.status.size.height)
|
||||||
currentAccount.followRequests.map(\.id).contains(notification.account.id)
|
}
|
||||||
{
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
FollowRequestButtons(account: notification.account)
|
makeMainLabel(type: notification.type)
|
||||||
}
|
makeContent(type: notification.type)
|
||||||
|
if notification.type == .follow_request,
|
||||||
|
currentAccount.followRequests.map(\.id).contains(notification.accounts[0].id)
|
||||||
|
{
|
||||||
|
FollowRequestButtons(account: notification.accounts[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeAvatarView(type: Models.Notification.NotificationType) -> some View {
|
private func makeAvatarView(type: Models.Notification.NotificationType) -> some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
AvatarView(url: notification.account.avatar)
|
AvatarView(url: notification.accounts[0].avatar)
|
||||||
ZStack(alignment: .center) {
|
makeNotificationIconView(type: type)
|
||||||
Circle()
|
.offset(x: -8, y: -8)
|
||||||
.strokeBorder(Color.white, lineWidth: 1)
|
|
||||||
.background(Circle().foregroundColor(theme.tintColor))
|
|
||||||
.frame(width: 24, height: 24)
|
|
||||||
|
|
||||||
Image(systemName: type.iconName())
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
|
||||||
.offset(x: -8, y: -8)
|
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: notification.account))
|
routerPath.navigate(to: .accountDetailWithAccount(account: notification.accounts[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeNotificationIconView(type: Models.Notification.NotificationType) -> some View {
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(Color.white, lineWidth: 1)
|
||||||
|
.background(Circle().foregroundColor(theme.tintColor))
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
|
||||||
|
Image(systemName: type.iconName())
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 12, height: 12)
|
||||||
|
.foregroundColor(.white)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeMainLabel(type: Models.Notification.NotificationType) -> some View {
|
private func makeMainLabel(type: Models.Notification.NotificationType) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
if notification.accounts.count > 1 {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack(spacing: 8) {
|
||||||
|
ForEach(notification.accounts) { account in
|
||||||
|
AvatarView(url: account.avatar)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
routerPath.navigate(to: .accountDetailWithAccount(account: account))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.leading, 1)
|
||||||
|
.frame(height: AvatarView.Size.status.size.height + 2)
|
||||||
|
}.offset(y: -1)
|
||||||
|
}
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
EmojiTextApp(.init(stringValue: notification.account.safeDisplayName),
|
EmojiTextApp(.init(stringValue: notification.accounts[0].safeDisplayName),
|
||||||
emojis: notification.account.emojis,
|
emojis: notification.accounts[0].emojis,
|
||||||
append: {
|
append: {
|
||||||
Text(" ") +
|
(notification.accounts.count > 1
|
||||||
|
? Text("notifications-others-count \(notification.accounts.count - 1)")
|
||||||
|
.font(.scaledSubheadline)
|
||||||
|
.fontWeight(.regular)
|
||||||
|
: Text(" ")) +
|
||||||
Text(type.label())
|
Text(type.label())
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
.fontWeight(.regular) +
|
.fontWeight(.regular) +
|
||||||
|
@ -75,8 +100,7 @@ struct NotificationRowView: View {
|
||||||
})
|
})
|
||||||
.font(.scaledSubheadline)
|
.font(.scaledSubheadline)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.lineLimit(1)
|
if let status = notification.status, notification.type == .mention {
|
||||||
if let status = notification.status, notification.supportedType == .mention {
|
|
||||||
Group {
|
Group {
|
||||||
Text(" ⸱ ")
|
Text(" ⸱ ")
|
||||||
Text(Image(systemName: status.visibility.iconName))
|
Text(Image(systemName: status.visibility.iconName))
|
||||||
|
@ -90,7 +114,7 @@ struct NotificationRowView: View {
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: notification.account))
|
routerPath.navigate(to: .accountDetailWithAccount(account: notification.accounts[0]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,13 +133,13 @@ struct NotificationRowView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Group {
|
Group {
|
||||||
Text("@\(notification.account.acct)")
|
Text("@\(notification.accounts[0].acct)")
|
||||||
.font(.scaledCallout)
|
.font(.scaledCallout)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
if type == .follow {
|
if type == .follow {
|
||||||
EmojiTextApp(notification.account.note,
|
EmojiTextApp(notification.accounts[0].note,
|
||||||
emojis: notification.account.emojis)
|
emojis: notification.accounts[0].emojis)
|
||||||
.lineLimit(3)
|
.lineLimit(3)
|
||||||
.font(.scaledCallout)
|
.font(.scaledCallout)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
|
@ -126,7 +150,7 @@ struct NotificationRowView: View {
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
routerPath.navigate(to: .accountDetailWithAccount(account: notification.account))
|
routerPath.navigate(to: .accountDetailWithAccount(account: notification.accounts[0]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ public struct NotificationsListView: View {
|
||||||
private var notificationsView: some View {
|
private var notificationsView: some View {
|
||||||
switch viewModel.state {
|
switch viewModel.state {
|
||||||
case .loading:
|
case .loading:
|
||||||
ForEach(Models.Notification.placeholders()) { notification in
|
ForEach(ConsolidatedNotification.placeholders()) { notification in
|
||||||
NotificationRowView(notification: notification)
|
NotificationRowView(notification: notification)
|
||||||
.redacted(reason: .placeholder)
|
.redacted(reason: .placeholder)
|
||||||
.padding(.leading, .layoutPadding + 4)
|
.padding(.leading, .layoutPadding + 4)
|
||||||
|
@ -100,15 +100,13 @@ public struct NotificationsListView: View {
|
||||||
message: "notifications.empty.message")
|
message: "notifications.empty.message")
|
||||||
} else {
|
} else {
|
||||||
ForEach(notifications) { notification in
|
ForEach(notifications) { notification in
|
||||||
if notification.supportedType != nil {
|
NotificationRowView(notification: notification)
|
||||||
NotificationRowView(notification: notification)
|
.padding(.leading, .layoutPadding + 4)
|
||||||
.padding(.leading, .layoutPadding + 4)
|
.padding(.trailing, .layoutPadding)
|
||||||
.padding(.trailing, .layoutPadding)
|
.padding(.top, 6)
|
||||||
.padding(.top, 6)
|
.padding(.bottom, 2)
|
||||||
.padding(.bottom, 2)
|
Divider()
|
||||||
Divider()
|
.padding(.vertical, .dividerPadding)
|
||||||
.padding(.vertical, .dividerPadding)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ class NotificationsViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
case loading
|
case loading
|
||||||
case display(notifications: [Models.Notification], nextPageState: State.PagingState)
|
case display(notifications: [ConsolidatedNotification], nextPageState: State.PagingState)
|
||||||
case error(error: Error)
|
case error(error: Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ class NotificationsViewModel: ObservableObject {
|
||||||
var client: Client? {
|
var client: Client? {
|
||||||
didSet {
|
didSet {
|
||||||
if oldValue != client {
|
if oldValue != client {
|
||||||
notifications = []
|
consolidatedNotifications = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ class NotificationsViewModel: ObservableObject {
|
||||||
@Published var selectedType: Models.Notification.NotificationType? {
|
@Published var selectedType: Models.Notification.NotificationType? {
|
||||||
didSet {
|
didSet {
|
||||||
if oldValue != selectedType {
|
if oldValue != selectedType {
|
||||||
notifications = []
|
consolidatedNotifications = []
|
||||||
Task {
|
Task {
|
||||||
await fetchNotifications()
|
await fetchNotifications()
|
||||||
}
|
}
|
||||||
|
@ -49,32 +49,34 @@ class NotificationsViewModel: ObservableObject {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private var notifications: [Models.Notification] = []
|
private var consolidatedNotifications: [ConsolidatedNotification] = []
|
||||||
|
|
||||||
func fetchNotifications() async {
|
func fetchNotifications() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
var nextPageState: State.PagingState = .hasNextPage
|
var nextPageState: State.PagingState = .hasNextPage
|
||||||
if notifications.isEmpty {
|
if consolidatedNotifications.isEmpty {
|
||||||
state = .loading
|
state = .loading
|
||||||
notifications = try await client.get(endpoint: Notifications.notifications(sinceId: nil,
|
let notifications: [Models.Notification] =
|
||||||
maxId: nil,
|
try await client.get(endpoint: Notifications.notifications(sinceId: nil,
|
||||||
types: queryTypes))
|
maxId: nil,
|
||||||
|
types: queryTypes))
|
||||||
|
consolidatedNotifications = notifications.consolidated()
|
||||||
nextPageState = notifications.count < 15 ? .none : .hasNextPage
|
nextPageState = notifications.count < 15 ? .none : .hasNextPage
|
||||||
} else if let first = notifications.first {
|
} else if let first = consolidatedNotifications.first {
|
||||||
var newNotifications: [Models.Notification] =
|
var newNotifications: [Models.Notification] =
|
||||||
try await client.get(endpoint: Notifications.notifications(sinceId: first.id,
|
try await client.get(endpoint: Notifications.notifications(sinceId: first.id,
|
||||||
maxId: nil,
|
maxId: nil,
|
||||||
types: queryTypes))
|
types: queryTypes))
|
||||||
nextPageState = notifications.count < 15 ? .none : .hasNextPage
|
nextPageState = consolidatedNotifications.notificationCount < 15 ? .none : .hasNextPage
|
||||||
newNotifications = newNotifications.filter { notification in
|
newNotifications = newNotifications.filter { notification in
|
||||||
!notifications.contains(where: { $0.id == notification.id })
|
!consolidatedNotifications.contains(where: { $0.id == notification.id })
|
||||||
}
|
}
|
||||||
notifications.insert(contentsOf: newNotifications, at: 0)
|
consolidatedNotifications.insert(contentsOf: newNotifications.consolidated(), at: 0)
|
||||||
}
|
}
|
||||||
withAnimation {
|
withAnimation {
|
||||||
state = .display(notifications: notifications,
|
state = .display(notifications: consolidatedNotifications,
|
||||||
nextPageState: notifications.isEmpty ? .none : nextPageState)
|
nextPageState: consolidatedNotifications.isEmpty ? .none : nextPageState)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
state = .error(error: error)
|
||||||
|
@ -84,14 +86,14 @@ class NotificationsViewModel: ObservableObject {
|
||||||
func fetchNextPage() async {
|
func fetchNextPage() async {
|
||||||
guard let client else { return }
|
guard let client else { return }
|
||||||
do {
|
do {
|
||||||
guard let lastId = notifications.last?.id else { return }
|
guard let lastId = consolidatedNotifications.last?.id else { return }
|
||||||
state = .display(notifications: notifications, nextPageState: .loadingNextPage)
|
state = .display(notifications: consolidatedNotifications, nextPageState: .loadingNextPage)
|
||||||
let newNotifications: [Models.Notification] =
|
let newNotifications: [Models.Notification] =
|
||||||
try await client.get(endpoint: Notifications.notifications(sinceId: nil,
|
try await client.get(endpoint: Notifications.notifications(sinceId: nil,
|
||||||
maxId: lastId,
|
maxId: lastId,
|
||||||
types: queryTypes))
|
types: queryTypes))
|
||||||
notifications.append(contentsOf: newNotifications)
|
consolidatedNotifications.append(contentsOf: newNotifications.consolidated())
|
||||||
state = .display(notifications: notifications, nextPageState: newNotifications.count < 15 ? .none : .hasNextPage)
|
state = .display(notifications: consolidatedNotifications, nextPageState: newNotifications.count < 15 ? .none : .hasNextPage)
|
||||||
} catch {
|
} catch {
|
||||||
state = .error(error: error)
|
state = .error(error: error)
|
||||||
}
|
}
|
||||||
|
@ -106,14 +108,77 @@ class NotificationsViewModel: ObservableObject {
|
||||||
|
|
||||||
func handleEvent(event: any StreamEvent) {
|
func handleEvent(event: any StreamEvent) {
|
||||||
if let event = event as? StreamEventNotification,
|
if let event = event as? StreamEventNotification,
|
||||||
!notifications.contains(where: { $0.id == event.notification.id })
|
!consolidatedNotifications.contains(where: { $0.id == event.notification.id })
|
||||||
{
|
{
|
||||||
if let selectedType, event.notification.type == selectedType.rawValue {
|
if let selectedType, event.notification.type == selectedType.rawValue {
|
||||||
notifications.insert(event.notification, at: 0)
|
consolidatedNotifications.insert(contentsOf: [event.notification].consolidated(),
|
||||||
|
at: 0)
|
||||||
} else if selectedType == nil {
|
} else if selectedType == nil {
|
||||||
notifications.insert(event.notification, at: 0)
|
consolidatedNotifications.insert(contentsOf: [event.notification].consolidated(),
|
||||||
|
at: 0)
|
||||||
}
|
}
|
||||||
state = .display(notifications: notifications, nextPageState: .hasNextPage)
|
state = .display(notifications: consolidatedNotifications, nextPageState: .hasNextPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ConsolidatedNotification: Identifiable {
|
||||||
|
let id: String
|
||||||
|
let type: Models.Notification.NotificationType
|
||||||
|
let createdAt: ServerDate
|
||||||
|
let accounts: [Account]
|
||||||
|
let status: Status?
|
||||||
|
|
||||||
|
static func placeholder() -> ConsolidatedNotification {
|
||||||
|
.init(id: 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() -> [ConsolidatedNotification] {
|
||||||
|
Dictionary(grouping: self) { notification -> String? in
|
||||||
|
guard let supportedType = notification.supportedType else { return nil }
|
||||||
|
|
||||||
|
switch supportedType {
|
||||||
|
case .follow:
|
||||||
|
// Always group followers
|
||||||
|
return supportedType.rawValue
|
||||||
|
case .reblog, .favourite:
|
||||||
|
// Group boosts and favourites by status
|
||||||
|
return "\(supportedType.rawValue)-\(notification.status?.id ?? "")"
|
||||||
|
case .follow_request, .poll, .status, .update, .mention:
|
||||||
|
// Never group those
|
||||||
|
return notification.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.values
|
||||||
|
.compactMap { notifications in
|
||||||
|
guard let notification = notifications.first,
|
||||||
|
let supportedType = notification.supportedType
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
|
return ConsolidatedNotification(id: notification.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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue