diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index fa51fbf404..6f96b8a2c9 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -158,7 +158,7 @@ final class ChatModel: ObservableObject { addChat(Chat(c), at: i) } } - NtfManager.shared.setNtfBadgeCount(totalUnreadCount()) + NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers()) } // func addGroup(_ group: SimpleXChat.Group) { @@ -172,7 +172,6 @@ final class ChatModel: ObservableObject { if case .rcvNew = cItem.meta.itemStatus { chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1 increaseUnreadCounter(user: currentUser!) - NtfManager.shared.incNtfBadgeCount() } if i > 0 { if chatId == nil { @@ -253,9 +252,6 @@ final class ChatModel: ObservableObject { // remove from current chat if chatId == cInfo.id { if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) { - if reversedChatItems[i].isRcvNew { - NtfManager.shared.decNtfBadgeCount() - } _ = withAnimation { self.reversedChatItems.remove(at: i) } @@ -304,7 +300,7 @@ final class ChatModel: ObservableObject { func markChatItemsRead(_ cInfo: ChatInfo) { // update preview _updateChat(cInfo.id) { chat in - NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount) + self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount) chat.chatStats = ChatStats() } // update current chat @@ -337,7 +333,6 @@ final class ChatModel: ObservableObject { // update preview let markedCount = chat.chatStats.unreadCount - unreadBelow if markedCount > 0 { - NtfManager.shared.decNtfBadgeCount(by: markedCount) chat.chatStats.unreadCount -= markedCount self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount) } @@ -357,7 +352,7 @@ final class ChatModel: ObservableObject { func clearChat(_ cInfo: ChatInfo) { // clear preview if let chat = getChat(cInfo.id) { - NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount) + self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount) chat.chatItems = [] chat.chatStats = ChatStats() chat.chatInfo = cInfo @@ -397,20 +392,23 @@ final class ChatModel: ObservableObject { func increaseUnreadCounter(user: User) { changeUnreadCounter(user: user, by: 1) + NtfManager.shared.incNtfBadgeCount() } func decreaseUnreadCounter(user: User, by: Int = 1) { changeUnreadCounter(user: user, by: -by) + NtfManager.shared.decNtfBadgeCount(by: by) } private func changeUnreadCounter(user: User, by: Int) { if let i = users.firstIndex(where: { $0.user.id == user.id }) { - users[i].unreadCount += Int64(by) + users[i].unreadCount += by } } - func totalUnreadCount() -> Int { - chats.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) + func totalUnreadCountForAllUsers() -> Int { + chats.filter { $0.chatInfo.ntfsEnabled }.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) + + users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount }) } func getPrevChatItem(_ ci: ChatItem) -> ChatItem? { diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 90e092701d..97252f1ade 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -39,10 +39,10 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)") if let userId = content.userInfo["userId"] as? Int64, userId != chatModel.currentUser?.userId { - changeActiveUser(userId) + changeActiveUser(userId) } if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact, - let chatId = content.userInfo["chatId"] as? String { + let chatId = content.userInfo["chatId"] as? String { if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo { Task { await acceptContactRequest(contactRequest) } } else { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 4026b7c2c1..35dbf0370c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -928,7 +928,7 @@ func startChat() throws { m.users = try listUsers() if justStarted { try getUserChatData() - NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount()) + NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers()) try refreshCallInvitations() (m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken() if let token = m.deviceToken { @@ -1077,7 +1077,7 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .newChatItem(user, aChatItem): if !active(user) { - if case .rcvNew = aChatItem.chatItem.meta.itemStatus { + if case .rcvNew = aChatItem.chatItem.meta.itemStatus, aChatItem.chatInfo.ntfsEnabled { m.increaseUnreadCounter(user: user) } return @@ -1125,7 +1125,7 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): if !active(user) { - if toChatItem == nil && deletedChatItem.chatItem.isRcvNew { + if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { m.decreaseUnreadCounter(user: user) } return diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 459cab70de..c8b641d20b 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -60,7 +60,7 @@ struct SimpleXApp: App { enteredBackground = ProcessInfo.processInfo.systemUptime } doAuthenticate = false - NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCount()) + NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers()) case .active: if chatModel.chatRunning == true { ChatReceiver.shared.start() diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift index 7da22919e9..962d783bcb 100644 --- a/apps/ios/Shared/Views/Call/CallManager.swift +++ b/apps/ios/Shared/Views/Call/CallManager.swift @@ -36,7 +36,6 @@ class CallManager { func answerIncomingCall(invitation: RcvCallInvitation) { let m = ChatModel.shared - // TODO: change active user m.callInvitations.removeValue(forKey: invitation.contact.id) m.activeCall = Call( direction: .incoming, diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index 22a14e4d5b..0044434efd 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -29,6 +29,10 @@ struct IncomingCallView: View { private func incomingCall(_ invitation: RcvCallInvitation) -> some View { VStack(alignment: .leading, spacing: 6) { HStack { + if m.users.count > 1 { + ProfileImage(imageStr: invitation.user.image, color: .white) + .frame(width: 24, height: 24) + } Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green) Text(invitation.callTypeText) } @@ -82,6 +86,8 @@ struct IncomingCallView: View { struct IncomingCallView_Previews: PreviewProvider { static var previews: some View { CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData - return IncomingCallView() + let m = ChatModel() + m.users = [UserInfo.sampleData, UserInfo.sampleData] + return IncomingCallView().environmentObject(m) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index fd94e96a33..62e2822065 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -233,7 +233,6 @@ struct ChatView: View { if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) { Task { await apiMarkChatItemRead(cInfo, ci) - NtfManager.shared.decNtfBadgeCount() } } } diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index 7078cdbfd1..9a151c8a90 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -132,8 +132,8 @@ struct UserPicker: View { } } -func unreadCounter(_ unread: Int64) -> some View { - unreadCountText(Int(truncatingIfNeeded: unread)) +func unreadCounter(_ unread: Int) -> some View { + unreadCountText(unread) .font(.caption) .foregroundColor(.white) .padding(.horizontal, 4) diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 211905255c..ce0a6160b8 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -51,13 +51,17 @@ struct CreateProfile: View { Spacer() HStack { - Button { - hideKeyboard() - withAnimation { m.onboardingStage = .step1_SimpleXInfo } - } label: { - HStack { - Image(systemName: "lessthan") - Text("About SimpleX") + if m.users.isEmpty { + Button { + hideKeyboard() + withAnimation { + m.onboardingStage = .step1_SimpleXInfo + } + } label: { + HStack { + Image(systemName: "lessthan") + Text("About SimpleX") + } } } diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 6971c3f447..9f1ffb731b 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -107,18 +107,16 @@ class NotificationService: UNNotificationServiceExtension { let encNtfInfo = ntfData["message"] as? String, let dbStatus = startChat() { if case .ok = dbStatus, - let ntfMsgInfos = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - for ntfMsgInfo in ntfMsgInfos { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)") - if let connEntity = ntfMsgInfo.connEntity { - setBestAttemptNtf(createConnectionEventNtf(ntfMsgInfo.user, connEntity)) - if let id = connEntity.id { - Task { - logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)") - await PendingNtfs.shared.createStream(id) - await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count) - deliverBestAttemptNtf() - } + let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)") + if let connEntity = ntfMsgInfo.connEntity { + setBestAttemptNtf(createConnectionEventNtf(ntfMsgInfo.user, connEntity)) + if let id = connEntity.id { + Task { + logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)") + await PendingNtfs.shared.createStream(id) + await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count) + deliverBestAttemptNtf() } } } @@ -220,6 +218,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification case let .newChatItem(user, aChatItem): let cInfo = aChatItem.chatInfo var cItem = aChatItem.chatItem + if !cInfo.ntfsEnabled { + ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1)) + } if case .image = cItem.content.msgContent { if let file = cItem.file, file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV, @@ -258,15 +259,6 @@ func updateNetCfg() { } } -func listUsers() -> [UserInfo] { - let r = sendSimpleXCmd(.listUsers) - logger.debug("listUsers sendSimpleXCmd response: \(String(describing: r))") - switch r { - case let .usersList(users): return users - default: return [] - } -} - func apiGetActiveUser() -> User? { let r = sendSimpleXCmd(.showActiveUser) logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))") @@ -300,21 +292,20 @@ func apiSetIncognito(incognito: Bool) throws { throw r } -func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> [NtfMessages]? { - let users = listUsers() - if users.isEmpty { - logger.debug("no users") - return [] +func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { + guard apiGetActiveUser() != nil else { + logger.debug("no active user") + return nil } - var result: [NtfMessages] = [] - users.forEach { - let r = sendSimpleXCmd(.apiGetNtfMessage(userId: $0.user.userId, nonce: nonce, encNtfInfo: encNtfInfo)) - if case let .ntfMessages(user, connEntity, msgTs, ntfMessages) = r { - result.append(NtfMessages(user: user, connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)) - } - logger.debug("apiGetNtfMessage ignored response: \(String.init(describing: r), privacy: .public)") + let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) + if case let .ntfMessages(user, connEntity, msgTs, ntfMessages) = r, let user = user { + return NtfMessages(user: user, connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages) + } else if case let .chatCmdError(_, error) = r { + logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") + } else { + logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)") } - return result + return nil } func apiReceiveFile(fileId: Int64, inline: Bool) -> AChatItem? { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d1b3dbab0d..eb03b9a27d 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -37,7 +37,7 @@ public enum ChatCommand { case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) case apiDeleteToken(token: DeviceToken) - case apiGetNtfMessage(userId: Int64, nonce: String, encNtfInfo: String) + case apiGetNtfMessage(nonce: String, encNtfInfo: String) case apiNewGroup(userId: Int64, groupProfile: GroupProfile) case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) case apiJoinGroup(groupId: Int64) @@ -125,7 +125,7 @@ public enum ChatCommand { case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" - case let .apiGetNtfMessage(userId, nonce, encNtfInfo): return "/_ntf message \(userId) \(nonce) \(encNtfInfo)" + case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" case let .apiNewGroup(userId, groupProfile): return "/_group \(userId) \(encodeJSON(groupProfile))" case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)" @@ -409,7 +409,7 @@ public enum ChatResponse: Decodable, Error { case callInvitations(callInvitations: [RcvCallInvitation]) case ntfTokenStatus(status: NtfTknStatus) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode) - case ntfMessages(user: User, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) + case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) case newContactConnection(user: User, connection: PendingContactConnection) case contactConnectionDeleted(user: User, connection: PendingContactConnection) case versionInfo(versionInfo: CoreVersionInfo) @@ -1078,6 +1078,7 @@ public enum ChatError: Decodable { public enum ChatErrorType: Decodable { case noActiveUser case activeUserExists + case differentActiveUser case chatNotStarted case invalidConnReq case invalidChatMessage(message: String) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 009462d979..8ae256cdda 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -36,9 +36,9 @@ public struct User: Decodable, NamedChat, Identifiable { public struct UserInfo: Decodable, Identifiable { public var user: User - public var unreadCount: Int64 + public var unreadCount: Int - public init(user: User, unreadCount: Int64) { + public init(user: User, unreadCount: Int) { self.user = user self.unreadCount = unreadCount }