diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 262c4a4b48..7dae94dfcd 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -74,6 +74,7 @@ final class ChatModel: ObservableObject { @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] @Published var groupMembersIndexes: Dictionary = [:] // groupMemberId to index in groupMembers list + @Published var membersLoaded = false // items in the terminal view @Published var showingTerminal = false @Published var terminalItems: [TerminalItem] = [] @@ -195,6 +196,18 @@ final class ChatModel: ObservableObject { return nil } + func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async { + let groupMembers = await apiListMembers(groupInfo.groupId) + await MainActor.run { + if chatId == groupInfo.id { + self.groupMembers = groupMembers.map { GMember.init($0) } + self.populateGroupMembersIndexes() + self.membersLoaded = true + updateView() + } + } + } + private func getChatIndex(_ id: String) -> Int? { chats.firstIndex(where: { $0.id == id }) } @@ -390,8 +403,8 @@ final class ChatModel: ObservableObject { } func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { - if cItem.isRcvNew { - decreaseUnreadCounter(cInfo) + if cItem.isRcvNew, let chatIndex = getChatIndex(cInfo.id) { + decreaseUnreadCounter(chatIndex) } // update previews if let chat = getChat(cInfo.id) { @@ -536,13 +549,18 @@ final class ChatModel: ObservableObject { } } - func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { - if chatId == cInfo.id, let i = getChatItemIndex(cItem) { - if reversedChatItems[i].isRcvNew { - // update current chat - markChatItemRead_(i) - // update preview - decreaseUnreadCounter(cInfo) + func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { + if chatId == cInfo.id, + let itemIndex = getChatItemIndex(cItem), + let chatIndex = getChatIndex(cInfo.id), + reversedChatItems[itemIndex].isRcvNew { + await MainActor.run { + withTransaction(Transaction()) { + // update current chat + markChatItemRead_(itemIndex) + // update preview + decreaseUnreadCounter(chatIndex) + } } } } @@ -558,11 +576,9 @@ final class ChatModel: ObservableObject { } } - func decreaseUnreadCounter(_ cInfo: ChatInfo) { - if let i = getChatIndex(cInfo.id) { - chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1 - decreaseUnreadCounter(user: currentUser!) - } + func decreaseUnreadCounter(_ chatIndex: Int) { + chats[chatIndex].chatStats.unreadCount = chats[chatIndex].chatStats.unreadCount - 1 + decreaseUnreadCounter(user: currentUser!) } func increaseUnreadCounter(user: any UserLike) { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 8c90c4b1ac..f1b413d663 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1207,7 +1207,7 @@ func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { do { logger.debug("apiMarkChatItemRead: \(cItem.id)") try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id)) - await MainActor.run { ChatModel.shared.markChatItemRead(cInfo, cItem) } + await ChatModel.shared.markChatItemRead(cInfo, cItem) } catch { logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))") } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 7e302d67d1..f8723fdba8 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -37,7 +37,6 @@ struct ChatView: View { @State private var searchText: String = "" @FocusState private var searchFocussed // opening GroupMemberInfoView on member icon - @State private var membersLoaded = false @State private var selectedMember: GMember? = nil // opening GroupLinkView on link button (incognito) @State private var showGroupLinkSheet: Bool = false @@ -122,7 +121,7 @@ struct ChatView: View { chatModel.reversedChatItems = [] chatModel.groupMembers = [] chatModel.groupMembersIndexes.removeAll() - membersLoaded = false + chatModel.membersLoaded = false } } } @@ -164,7 +163,7 @@ struct ChatView: View { } } else if case let .group(groupInfo) = cInfo { Button { - Task { await loadGroupMembers(groupInfo) { showChatInfoSheet = true } } + Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } } } label: { ChatInfoToolbar(chat: chat) .tint(theme.colors.primary) @@ -250,19 +249,7 @@ struct ChatView: View { } } } - - private func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async { - let groupMembers = await apiListMembers(groupInfo.groupId) - await MainActor.run { - if chatModel.chatId == groupInfo.id { - chatModel.groupMembers = groupMembers.map { GMember.init($0) } - chatModel.populateGroupMembersIndexes() - membersLoaded = true - updateView() - } - } - } - + private func initChatView() { let cInfo = chat.chatInfo // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed. @@ -477,9 +464,7 @@ struct ChatView: View { .foregroundColor(theme.colors.primary) } .onTapGesture { - if let latestUnreadItem = filtered(chatModel.reversedChatItems).last(where: { $0.isRcvNew }) { - scrollModel.scrollToItem(id: latestUnreadItem.id) - } + scrollModel.scrollToBottom() } } else if !counts.isNearBottom { circleButton { @@ -534,7 +519,7 @@ struct ChatView: View { private func addMembersButton() -> some View { Button { if case let .group(gInfo) = chat.chatInfo { - Task { await loadGroupMembers(gInfo) { showAddMembersSheet = true } } + Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } } } } label: { Image(systemName: "person.crop.circle.badge.plus") @@ -604,11 +589,9 @@ struct ChatView: View { chat: chat, chatItem: ci, maxWidth: maxWidth, - itemWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, - revealedChatItem: $revealedChatItem, - chatView: self + revealedChatItem: $revealedChatItem ) } @@ -616,13 +599,11 @@ struct ChatView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat - var chatItem: ChatItem - var maxWidth: CGFloat - @State var itemWidth: CGFloat + let chatItem: ChatItem + let maxWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? @Binding var revealedChatItem: ChatItem? - var chatView: ChatView @State private var deletingItem: ChatItem? = nil @State private var showDeleteMessage = false @@ -700,11 +681,11 @@ struct ChatView: View { HStack(alignment: .top, spacing: 8) { ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background) .onTapGesture { - if chatView.membersLoaded { + if m.membersLoaded { selectedMember = m.getGroupMember(member.groupMemberId) } else { Task { - await chatView.loadGroupMembers(groupInfo) { + await m.loadGroupMembers(groupInfo) { selectedMember = m.getGroupMember(member.groupMemberId) } } @@ -1099,7 +1080,7 @@ struct ChatView: View { chatItemInfo = ciInfo } if case let .group(gInfo) = chat.chatInfo { - await chatView.loadGroupMembers(gInfo) + await m.loadGroupMembers(gInfo) } } catch let error { logger.error("apiGetChatItemInfo error: \(responseError(error))")