99import UIKit
1010import Habitica_Models
1111import SwiftUI
12+ import ReactiveSwift
13+ import Kingfisher
1214
1315class NotificationsViewModel : ViewModel {
1416 var onDismiss : ( ( @escaping ( ) -> Void ) -> Void ) ?
1517
1618 private let userRepository = UserRepository ( )
1719 private let socialRepository = SocialRepository ( )
20+ private let inventoryRepository = InventoryRepository ( )
1821 @Published var notifications : [ NotificationProtocol ] = [ ]
1922 @Published var partyID : String ?
23+ @Published var inviterNames : [ String : String ? ] = [ : ]
24+ @Published var quests : [ String : QuestProtocol ? ] = [ : ]
2025
2126 override init ( ) {
2227 super. init ( )
23- disposable. add ( userRepository. getNotifications ( ) . on ( value: { [ weak self] ( entries, _) in
28+ disposable. add ( userRepository. getNotifications ( )
29+ . on ( value: { [ weak self] ( entries, _) in
2430 if self ? . notifications. isEmpty == true {
2531 self ? . notifications = entries
2632 } else {
2733 withAnimation {
2834 self ? . notifications = entries
2935 }
3036 }
37+ entries. forEach { notification in
38+ if notification. type == . groupInvite {
39+ if let groupInvite = notification as? NotificationGroupInviteProtocol , let id = groupInvite. inviterID {
40+ if self ? . inviterNames. keys. contains ( id) != true {
41+ self ? . getInviter ( id: id)
42+ }
43+ }
44+ } else if notification. type == . questInvite {
45+ if let questInvite = notification as? NotificationQuestInviteProtocol , let key = questInvite. questKey {
46+ if self ? . quests. keys. contains ( key) != true {
47+ self ? . getQuest ( key: key)
48+ }
49+ }
50+ }
51+ }
3152 } ) . start ( ) )
3253 disposable. add ( userRepository. getUser ( ) . map { $0. party? . id } . skipRepeats ( )
3354 . on ( value: { [ weak self] partyID in
@@ -49,24 +70,37 @@ class NotificationsViewModel: ViewModel {
4970 disposable. add ( userRepository. readNotifications ( notifications: dismissableNotifications) . observeCompleted { } )
5071 }
5172
73+ func updateNotifications( ) {
74+ disposable. add ( userRepository. retrieveUser ( forced: true ) . observeCompleted {
75+ } )
76+ }
77+
5278 func decline( notification: NotificationProtocol ) {
5379 if notification. type == . groupInvite, let notification = notification as? NotificationGroupInviteProtocol {
54- socialRepository. rejectGroupInvitation ( groupID: notification. groupID ?? " " ) . observeCompleted { }
80+ socialRepository. rejectGroupInvitation ( groupID: notification. groupID ?? " " ) . observeCompleted {
81+ self . updateNotifications ( )
82+ }
5583 } else if notification. type == . questInvite && notification is NotificationQuestInviteProtocol {
56- socialRepository. rejectQuestInvitation ( groupID: " party " ) . observeCompleted { }
84+ socialRepository. rejectQuestInvitation ( groupID: " party " ) . observeCompleted {
85+ self . updateNotifications ( )
86+ }
5787 }
5888 }
5989
6090 func accept( notification: NotificationProtocol ) {
6191 if notification. type == . groupInvite, let notification = notification as? NotificationGroupInviteProtocol {
62- socialRepository. joinGroup ( groupID: notification. groupID ?? " " , isParty: notification. isParty) . observeCompleted { }
92+ socialRepository. joinGroup ( groupID: notification. groupID ?? " " , isParty: notification. isParty) . observeCompleted {
93+ self . updateNotifications ( )
94+ }
6395 } else if notification. type == . questInvite && notification is NotificationQuestInviteProtocol {
64- socialRepository. acceptQuestInvitation ( groupID: " party " ) . observeCompleted { }
96+ socialRepository. acceptQuestInvitation ( groupID: " party " ) . observeCompleted {
97+ self . updateNotifications ( )
98+ }
99+
65100 }
66101 }
67102
68103 func openNotification( notification: NotificationProtocol ) {
69- // This could be handled better
70104 var url : String ?
71105 switch notification. type {
72106 case . groupInvite:
@@ -91,12 +125,23 @@ class NotificationsViewModel: ViewModel {
91125 case . newStuff:
92126 url = " /static/new-stuff "
93127 case . itemReceived:
94- let itemReceivedNotification = notification as? NotificationItemReceivedProtocol
95- if itemReceivedNotification? . openDestination? . starts ( with: " / " ) == true {
96- url = itemReceivedNotification? . openDestination
97- break
128+ url = openItemReceivedNotification ( notification: notification as? NotificationItemReceivedProtocol )
129+ default :
130+ break
131+ }
132+ if let url = url, let onDismiss = onDismiss {
133+ onDismiss {
134+ RouterHandler . shared. handle ( urlString: url)
98135 }
99- switch itemReceivedNotification? . openDestination {
136+ }
137+ }
138+
139+ private func openItemReceivedNotification( notification: NotificationItemReceivedProtocol ? ) -> String ? {
140+ let url : String ?
141+ if notification? . openDestination? . starts ( with: " / " ) == true {
142+ url = notification? . openDestination
143+ } else {
144+ switch notification? . openDestination {
100145 case " equipment " :
101146 url = " /inventory/equipment "
102147 case " customization " :
@@ -106,14 +151,24 @@ class NotificationsViewModel: ViewModel {
106151 default :
107152 url = " /inventory/items "
108153 }
109- default :
110- break
111154 }
112- if let url = url, let onDismiss = onDismiss {
113- onDismiss {
114- RouterHandler . shared. handle ( urlString: url)
155+ return url
156+ }
157+
158+ private func getInviter( id: String ) {
159+ inviterNames [ id] = nil
160+ disposable. add ( socialRepository. retrieveMember ( userID: id) . observeValues ( { [ weak self] member in
161+ if let name = member? . profile? . name ?? member? . username {
162+ self ? . inviterNames [ id] = name
115163 }
116- }
164+ } ) )
165+ }
166+
167+ private func getQuest( key: String ) {
168+ quests [ key] = nil
169+ disposable. add ( inventoryRepository. getQuest ( key: key) . take ( first: 1 ) . on ( value: { [ weak self] quest in
170+ self ? . quests [ key] = quest
171+ } ) . start ( ) )
117172 }
118173}
119174
@@ -199,11 +254,11 @@ struct NotificationResponseView: View {
199254
200255 var body : some View {
201256 HStack ( spacing: 12 ) {
202- HabiticaButtonUI ( label: Image ( systemName: . xmark) . foregroundStyle ( . red1) , color: . red100, onTap: {
203-
257+ HabiticaButtonUI ( label: Image ( systemName: . xmark) . foregroundStyle ( . red1) , color: . red100, size : . small , onTap: {
258+ onDecline ( )
204259 } )
205- HabiticaButtonUI ( label: Image ( systemName: . xmark ) . foregroundStyle ( . green1) , color: . green100, onTap: {
206-
260+ HabiticaButtonUI ( label: Image ( systemName: . checkmark ) . foregroundStyle ( . green1) , color: . green100, size : . small , onTap: {
261+ onAccept ( )
207262 } )
208263 } . scaledFont ( size: 17 , weight: . medium)
209264 }
@@ -308,21 +363,72 @@ struct NewMysteryItemNotificationView: View {
308363
309364struct QuestInviteNotificationView : View {
310365 let notification : NotificationQuestInviteProtocol
366+ let quest : QuestProtocol ?
311367 let onDecline : ( ) -> Void
312368 let onAccept : ( ) -> Void
313369
314370 var body : some View {
315- NotificationMainContent ( content: {
316- NotificationImage ( content: Image ( . notificationsQuest) )
317- NotificationTexts ( title: Text ( " " ) )
318- } )
319- NotificationResponseView ( onDecline: onDecline, onAccept: onAccept)
371+ VStack ( spacing: 8 ) {
372+ if let quest = quest {
373+ NotificationMainContent ( content: {
374+ NotificationImage ( content: Image ( . notificationsQuest) )
375+ NotificationTexts ( description: Text ( markdown: L10n . Notifications. questInvite ( quest. text ?? " " ) ) )
376+ } )
377+ VStack ( spacing: 12 ) {
378+ HStack {
379+ if let boss = quest. boss {
380+ Text ( L10n . boss)
381+ Spacer ( )
382+ HStack ( spacing: 4 ) {
383+ Text ( " \( boss. health) " ) . foregroundStyle ( . red1)
384+ Image ( uiImage: HabiticaIcons . imageOfHeartLightBg)
385+ } . padding ( . horizontal, 11 )
386+ . padding ( . vertical, 6 )
387+ . background ( . red500)
388+ . cornerRadius ( UIConstants . mediumCornerRadius)
389+ } else if let collect = quest. collect {
390+ Text ( L10n . collect)
391+ Spacer ( )
392+ HStack ( spacing: 2 ) {
393+ ForEach ( collect, id: \. key) { item in
394+ KFImage ( ImageManager . buildImageUrl ( name: " quest_ \( quest. key ?? " " ) _ \( item. key ?? " " ) " ) )
395+ . resizable ( )
396+ . interpolation ( . none)
397+ . frame ( width: 25 , height: 25 )
398+ }
399+ let sum = collect. map { $0. count } . reduce ( 0 , + )
400+ Text ( " \( sum) " )
401+ . padding ( 4 )
402+ . background ( Color ( ThemeService . shared. theme. offsetBackgroundColor) )
403+ . cornerRadius ( UIConstants . mediumCornerRadius)
404+ }
405+ }
406+ }
407+ HStack {
408+ Text ( L10n . difficulty)
409+ Spacer ( )
410+ Image ( uiImage: HabiticaIcons . imageOfDifficultyStars ( difficulty: CGFloat ( quest. boss? . strength ?? 1 ) ) )
411+ . padding ( . horizontal, 11 )
412+ . padding ( . vertical, 6 )
413+ . background ( Color ( ThemeService . shared. theme. offsetBackgroundColor) )
414+ . cornerRadius ( UIConstants . mediumCornerRadius)
415+ }
416+ }
417+ . font ( . system( size: 15 , weight: . semibold) )
418+ . foregroundStyle ( Color ( ThemeService . shared. theme. primaryTextColor) )
419+ . padding ( 11 )
420+ . background ( Color ( ThemeService . shared. theme. contentBackgroundColor) )
421+ . cornerRadius ( UIConstants . mediumCornerRadius)
422+
423+ }
424+ NotificationResponseView ( onDecline: onDecline, onAccept: onAccept)
425+ }
320426 }
321427}
322428
323429struct GroupInviteNotificationView : View {
324430 let notification : NotificationGroupInviteProtocol
325- let isPartyInvite : Bool
431+ let inviterName : String ?
326432 let onDecline : ( ) -> Void
327433 let onAccept : ( ) -> Void
328434
@@ -345,11 +451,13 @@ struct GroupInviteNotificationView: View {
345451 }
346452
347453 var body : some View {
348- NotificationMainContent {
349- NotificationImage ( content: Image ( . notificationsGuild) )
350- NotificationTexts ( title: Text ( getTitleFor ( groupName: notification. groupName ?? " " , inviterName: nil , isPartyInvitation: isPartyInvite) ) )
454+ VStack ( spacing: 0 ) {
455+ NotificationMainContent {
456+ NotificationImage ( content: Image ( . notificationsGuild) )
457+ NotificationTexts ( description: Text ( markdown: getTitleFor ( groupName: notification. groupName ?? " " , inviterName: inviterName, isPartyInvitation: notification. isParty) ) )
458+ }
459+ NotificationResponseView ( onDecline: onDecline, onAccept: onAccept)
351460 }
352- NotificationResponseView ( onDecline: onDecline, onAccept: onAccept)
353461 }
354462}
355463
@@ -400,13 +508,13 @@ struct NotificationsPage: View {
400508 } else if type == . newMysteryItem, let notification = notification as? NotificationNewMysteryItemProtocol {
401509 NewMysteryItemNotificationView ( notification: notification, onDismiss: onNotificationDismiss)
402510 } else if type == . questInvite, let notification = notification as? NotificationQuestInviteProtocol {
403- QuestInviteNotificationView ( notification: notification, onDecline: {
511+ QuestInviteNotificationView ( notification: notification, quest : viewModel . quests [ notification . questKey ?? " " ] ?? nil , onDecline: {
404512 viewModel. decline ( notification: notification)
405513 } , onAccept: {
406514 viewModel. accept ( notification: notification)
407515 } )
408516 } else if type == . groupInvite, let notification = notification as? NotificationGroupInviteProtocol {
409- GroupInviteNotificationView ( notification: notification, isPartyInvite : notification. groupID == viewModel . partyID , onDecline: {
517+ GroupInviteNotificationView ( notification: notification, inviterName : viewModel . inviterNames [ notification. inviterID ?? " " ] ?? nil , onDecline: {
410518 viewModel. decline ( notification: notification)
411519 } , onAccept: {
412520 viewModel. accept ( notification: notification)
@@ -429,7 +537,7 @@ struct NotificationsPage: View {
429537 } else {
430538 ScrollView {
431539 LazyVStack ( spacing: 8 ) {
432- ForEach ( viewModel. notifications, id: \. safeId ) { notification in
540+ ForEach ( viewModel. notifications, id: \. id ) { notification in
433541 if notification. isValid {
434542 renderNotification ( notification: notification)
435543 . foregroundStyle ( Color ( ThemeService . shared. theme. primaryTextColor) )
0 commit comments