Skip to content

Commit fc0ca85

Browse files
committed
Implement missing notification display
1 parent 023c307 commit fc0ca85

File tree

5 files changed

+154
-40
lines changed

5 files changed

+154
-40
lines changed

HabitRPG/Generated/Strings.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ public enum L10n {
100100
}
101101
/// Body Size
102102
public static var bodySize: String { return L10n.tr("Mainstrings", "body_size") }
103+
/// Boss
104+
public static var boss: String { return L10n.tr("Mainstrings", "boss") }
103105
/// Broad
104106
public static var broad: String { return L10n.tr("Mainstrings", "broad") }
105107
/// Broken Challenge
@@ -2061,11 +2063,11 @@ public enum L10n {
20612063
public static var groupPlanSettings: String { return L10n.tr("Mainstrings", "groups.group_plan_settings") }
20622064
/// Groups
20632065
public static var groups: String { return L10n.tr("Mainstrings", "groups.groups") }
2064-
/// **@%@** invited you to join the Guild: **%@**
2066+
/// **@%@** invited you to join the Goup **%@**
20652067
public static func guildInvitationInvitername(_ p1: String, _ p2: String) -> String {
20662068
return L10n.tr("Mainstrings", "groups.guild_invitation_invitername", p1, p2)
20672069
}
2068-
/// Someone invited you to join the Guild: **%@**
2070+
/// Someone invited you to join the Group **%@**
20692071
public static func guildInvitationNoInvitername(_ p1: String) -> String {
20702072
return L10n.tr("Mainstrings", "groups.guild_invitation_no_invitername", p1)
20712073
}

HabitRPG/Strings/Base.lproj/Mainstrings.strings

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@
605605
"quests.accepted" = "Accepted";
606606
"quests.rejected" = "Rejected";
607607
"quests.pending" = "Pending";
608+
"boss" = "Boss";
608609
"quests.confirm_cancel_invitation" = "Are you sure you want to cancel this quest? All invitation acceptances will be lost. The quest owner will retain possession of the quest scroll.";
609610
"quests.confirm_force_start" = "Are you sure? Not all party members have joined this quest! Quests start automatically when all players have joined or rejected the invitation.";
610611
"quests.confirm_abort" = "Are you sure you want to abort this mission? It will abort it for everyone in your party and all progress will be lost. The quest scroll will be returned to the quest owner.";
@@ -624,8 +625,8 @@
624625
"quests.unlock_previous_short" = "Finish Quest %d";
625626
"groups.leader_challenges" = "Only leader can create Challenges";
626627
"groups.assign_new_leader" = "Assign new Leader";
627-
"groups.guild_invitation_no_invitername" = "Someone invited you to join the Guild: **%@**";
628-
"groups.guild_invitation_invitername" = "**@%@** invited you to join the Guild: **%@**";
628+
"groups.guild_invitation_no_invitername" = "Someone invited you to join the Group **%@**";
629+
"groups.guild_invitation_invitername" = "**@%@** invited you to join the Goup **%@**";
629630
"groups.groups" = "Groups";
630631
"groups.group" = "Group";
631632
"groups.group_plan_settings" = "Group Plan Settings";

HabitRPG/UI/General/NotificationsTableViewController.swift

Lines changed: 142 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,46 @@
99
import UIKit
1010
import Habitica_Models
1111
import SwiftUI
12+
import ReactiveSwift
13+
import Kingfisher
1214

1315
class 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

309364
struct 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

323429
struct 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))

HabitRPG/UI/Social/Party/SendPartyInviteViewController.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ struct InviteView: View {
9292
}, label: {
9393
Image(systemName: .xmark).frame(width: 14, height: 14).foregroundStyle(Color(themeService.theme.primaryTextColor))
9494
}).frame(width: 30, height: 48)
95-
FocusableTextField(placeholder: "Username or email address", text: $text, isFirstResponder: $isFirstResponder).frame(height: 48)
95+
FocusableTextField(placeholder: "Username or email address", text: $text, isFirstResponder: $isFirstResponder, configuration: { textfield in
96+
textfield.autocapitalizationType = .none
97+
})
98+
.frame(height: 48)
9699
}.background(Color(themeService.theme.windowBackgroundColor).cornerRadius(UIConstants.mediumCornerRadius))
97100
.transition(.opacity)
98101
}

Habitica Database/Habitica Database/Repositories/UserLocalRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ public class UserLocalRepository: BaseLocalRepository {
356356

357357
public func getNotifications(userID: String) -> SignalProducer<ReactiveResults<[NotificationProtocol]>, ReactiveSwiftRealmError> {
358358
return RealmNotification.findBy(query: "userID == '\(userID)' && realmType != ''").sorted(key: "priority").reactive().map({ (value, changeset) -> ReactiveResults<[NotificationProtocol]> in
359-
return (value.map({ (notification) -> NotificationProtocol in return notification }), changeset)
359+
return (value.map({ (notification) -> NotificationProtocol in return notification.freeze() }), changeset)
360360
})
361361
}
362362

0 commit comments

Comments
 (0)