diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 2763c76857..d330e154b6 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -54,12 +54,50 @@ class ItemsModel: ObservableObject { willSet { publisher.send() } } + // Publishes directly to `objectWillChange` publisher, + // this will cause reversedChatItems to be rendered without throttling + @Published var isLoading = false + @Published var showLoadingProgress = false + init() { publisher .throttle(for: 0.25, scheduler: DispatchQueue.main, latest: true) .sink { self.objectWillChange.send() } .store(in: &bag) } + + func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) { + let navigationTimeout = Task { + do { + try await Task.sleep(nanoseconds: 250_000000) + await MainActor.run { + willNavigate() + ChatModel.shared.chatId = chatId + } + } catch {} + } + let progressTimeout = Task { + do { + try await Task.sleep(nanoseconds: 1500_000000) + await MainActor.run { showLoadingProgress = true } + } catch {} + } + Task { + if let chat = ChatModel.shared.getChat(chatId) { + await MainActor.run { self.isLoading = true } +// try? await Task.sleep(nanoseconds: 5000_000000) + await loadChat(chat: chat) + navigationTimeout.cancel() + progressTimeout.cancel() + await MainActor.run { + self.isLoading = false + self.showLoadingProgress = false + willNavigate() + ChatModel.shared.chatId = chatId + } + } + } + } } final class ChatModel: ObservableObject { diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index bb938cd231..95063845f1 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -57,7 +57,9 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { chatModel.ntfCallInvitationAction = (chatId, ntfAction) } } else { - chatModel.chatId = content.targetContentIdentifier + if let chatId = content.targetContentIdentifier { + ItemsModel.shared.loadOpenChat(chatId) + } } handler() } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 1f6edaf720..2f1abe44ce 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -320,8 +320,8 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] { let loadItemsPerPage = 50 -func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat { - let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search)) +func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> Chat { + let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search)) if case let .apiChat(_, chat) = r { return Chat.init(chat) } throw r } @@ -332,16 +332,18 @@ func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, sear throw r } -func loadChat(chat: Chat, search: String = "") { +func loadChat(chat: Chat, search: String = "") async { do { let cInfo = chat.chatInfo let m = ChatModel.shared let im = ItemsModel.shared m.chatItemStatuses = [:] - im.reversedChatItems = [] - let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) - m.updateChatInfo(chat.chatInfo) - im.reversedChatItems = chat.chatItems.reversed() + await MainActor.run { im.reversedChatItems = [] } + let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) + await MainActor.run { + im.reversedChatItems = chat.chatItems.reversed() + m.updateChatInfo(chat.chatInfo) + } } catch let error { logger.error("loadChat error: \(responseError(error))") } @@ -701,7 +703,7 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi return ((.contact, connection), nil) case let .contactAlreadyExists(_, contact): if let c = m.getContactChat(contact.contactId) { - await MainActor.run { m.chatId = c.id } + ItemsModel.shared.loadOpenChat(c.id) } let alert = contactAlreadyExistsAlert(contact) return (nil, alert) @@ -1170,7 +1172,7 @@ func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) a if contact.sndReady { DispatchQueue.main.async { dismissAllSheets(animated: true) { - ChatModel.shared.chatId = chat.id + ItemsModel.shared.loadOpenChat(chat.id) } } } @@ -1736,7 +1738,7 @@ func processReceivedMsg(_ res: ChatResponse) async { if active(user) && m.hasChat(mergedContact.id) { await MainActor.run { if m.chatId == mergedContact.id { - m.chatId = intoContact.id + ItemsModel.shared.loadOpenChat(mergedContact.id) } m.removeChat(mergedContact.id) } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index ae539d7298..fe306b944f 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -136,7 +136,7 @@ struct SimpleXApp: App { chatModel.updateChats(with: chats) if let id = chatModel.chatId, let chat = chatModel.getChat(id) { - loadChat(chat: chat) + Task { await loadChat(chat: chat) } } if let ncr = chatModel.ntfContactRequest { chatModel.ntfContactRequest = nil diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index 7c405b6346..32993d1a76 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -97,7 +97,7 @@ struct ChatItemForwardingView: View { ) } else { composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo) - chatModel.chatId = chat.id + ItemsModel.shared.loadOpenChat(chat.id) } } } label: { diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index dc917e7427..f6a856dad1 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -351,7 +351,7 @@ struct ChatItemInfoView: View { Button { Task { await MainActor.run { - chatModel.chatId = forwardedFromItem.chatInfo.id + ItemsModel.shared.loadOpenChat(forwardedFromItem.chatInfo.id) dismiss() } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 11b6f9aba3..e460ce00d6 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -59,7 +59,7 @@ struct ChatView: View { viewBody } } - + @ViewBuilder private var viewBody: some View { let cInfo = chat.chatInfo @@ -130,16 +130,23 @@ struct ChatView: View { } } } + .appSheet(item: $selectedMember) { member in + Group { + if case let .group(groupInfo) = chat.chatInfo { + GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) + } + } + } .onAppear { - loadChat(chat: chat) - initChatView() selectedChatItems = nil + initChatView() } .onChange(of: chatModel.chatId) { cId in showChatInfoSheet = false + selectedChatItems = nil + scrollModel.scrollToBottom() stopAudioPlayer() if let cId { - selectedChatItems = nil if let c = chatModel.getChat(cId) { chat = c } @@ -152,8 +159,10 @@ struct ChatView: View { .onChange(of: revealedChatItem) { _ in NotificationCenter.postReverseListNeedsLayout() } - .onChange(of: im.reversedChatItems) { reversedChatItems in - if reversedChatItems.count <= loadItemsPerPage && filtered(reversedChatItems).count < 10 { + .onChange(of: im.isLoading) { isLoading in + if !isLoading, + im.reversedChatItems.count <= loadItemsPerPage, + filtered(im.reversedChatItems).count < 10 { loadChatItems(chat.chatInfo) } } @@ -221,6 +230,7 @@ struct ChatView: View { } } ToolbarItem(placement: .navigationBarTrailing) { + let isLoading = im.isLoading && im.showLoadingProgress if selectedChatItems != nil { Button { withAnimation { @@ -243,19 +253,23 @@ struct ChatView: View { } } Menu { - if callsPrefEnabled && chatModel.activeCall == nil { - Button { - CallController.shared.startCall(contact, .video) - } label: { - Label("Video call", systemImage: "video") + if !isLoading { + if callsPrefEnabled && chatModel.activeCall == nil { + Button { + CallController.shared.startCall(contact, .video) + } label: { + Label("Video call", systemImage: "video") + } + .disabled(!contact.ready || !contact.active) } - .disabled(!contact.ready || !contact.active) + searchButton() + ToggleNtfsButton(chat: chat) + .disabled(!contact.ready || !contact.active) } - searchButton() - ToggleNtfsButton(chat: chat) - .disabled(!contact.ready || !contact.active) } label: { Image(systemName: "ellipsis") + .tint(isLoading ? Color.clear : nil) + .overlay { if isLoading { ProgressView() } } } } case let .group(groupInfo): @@ -280,10 +294,14 @@ struct ChatView: View { } } Menu { - searchButton() - ToggleNtfsButton(chat: chat) + if !isLoading { + searchButton() + ToggleNtfsButton(chat: chat) + } } label: { Image(systemName: "ellipsis") + .tint(isLoading ? Color.clear : nil) + .overlay { if isLoading { ProgressView() } } } } case .local: @@ -349,9 +367,7 @@ struct ChatView: View { searchText = "" searchMode = false searchFocussed = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - loadChat(chat: chat) - } + Task { await loadChat(chat: chat) } } } .padding(.horizontal) @@ -408,18 +424,11 @@ struct ChatView: View { } loadPage: { loadChatItems(cInfo) } + .opacity(ItemsModel.shared.isLoading ? 0 : 1) .padding(.vertical, -InvertedTableView.inset) .onTapGesture { hideKeyboard() } .onChange(of: searchText) { _ in - loadChat(chat: chat, search: searchText) - } - .onChange(of: chatModel.chatId) { chatId in - if let chatId, let c = chatModel.getChat(chatId) { - chat = c - showChatInfoSheet = false - loadChat(chat: c) - scrollModel.scrollToBottom() - } + Task { await loadChat(chat: chat, search: searchText) } } .onChange(of: im.reversedChatItems) { _ in floatingButtonModel.chatItemsChanged() @@ -815,8 +824,8 @@ struct ChatView: View { HStack(alignment: .top, spacing: 8) { MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background) .onTapGesture { - if m.membersLoaded { - selectedMember = m.getGroupMember(member.groupMemberId) + if let member = m.getGroupMember(member.groupMemberId) { + selectedMember = member } else { Task { await m.loadGroupMembers(groupInfo) { @@ -825,9 +834,6 @@ struct ChatView: View { } } } - .appSheet(item: $selectedMember) { member in - GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) - } chatItemWithMenu(ci, range, maxWidth) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index c9f34a61c0..436099261b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -321,24 +321,24 @@ struct GroupMemberInfoView: View { func knownDirectChatButton(_ chat: Chat, width: CGFloat) -> some View { InfoViewButton(image: "message.fill", title: "message", width: width) { - dismissAllSheets(animated: true) - DispatchQueue.main.async { - chatModel.chatId = chat.id + ItemsModel.shared.loadOpenChat(chat.id) { + dismissAllSheets(animated: true) } } } func newDirectChatButton(_ contactId: Int64, width: CGFloat) -> some View { InfoViewButton(image: "message.fill", title: "message", width: width) { - do { - let chat = try apiGetChat(type: .direct, id: contactId) - chatModel.addChat(chat) - dismissAllSheets(animated: true) - DispatchQueue.main.async { - chatModel.chatId = chat.id + Task { + do { + let chat = try await apiGetChat(type: .direct, id: contactId) + chatModel.addChat(chat) + ItemsModel.shared.loadOpenChat(chat.id) { + dismissAllSheets(animated: true) + } + } catch let error { + logger.error("openDirectChatButton apiGetChat error: \(responseError(error))") } - } catch let error { - logger.error("openDirectChatButton apiGetChat error: \(responseError(error))") } } } @@ -352,8 +352,9 @@ struct GroupMemberInfoView: View { await MainActor.run { progressIndicator = false chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) - dismissAllSheets(animated: true) - chatModel.chatId = memberContact.id + ItemsModel.shared.loadOpenChat(memberContact.id) { + dismissAllSheets(animated: true) + } chatModel.setContactNetworkStatus(memberContact, .connected) } } catch let error { diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index 97638a21c7..4ce6a1a065 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -152,18 +152,23 @@ struct ReverseList: UIV /// Scrolls to Item at index path /// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil` func scroll(to index: Int?, position: UITableView.ScrollPosition) { - if let index { - var animated = false - if #available(iOS 16.0, *) { - animated = true - } + var animated = false + if #available(iOS 16.0, *) { + animated = true + } + if let index, tableView.numberOfRows(inSection: 0) != 0 { tableView.scrollToRow( at: IndexPath(row: index, section: 0), at: position, animated: animated ) - Task { representer.scrollState = .atDestination } + } else { + tableView.setContentOffset( + CGPoint(x: .zero, y: -InvertedTableView.inset), + animated: animated + ) } + Task { representer.scrollState = .atDestination } } func update(items: Array) { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 3de6c93101..c8424ef332 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -114,7 +114,7 @@ struct ChatListNavLink: View { } } else { NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) } ) @@ -194,7 +194,7 @@ struct ChatListNavLink: View { } default: NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !groupInfo.ready @@ -221,7 +221,7 @@ struct ChatListNavLink: View { @ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !noteFolder.ready @@ -472,9 +472,7 @@ struct ChatListNavLink: View { Task { let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { AlertManager.shared.showAlert($0) }) if ok { - await MainActor.run { - chatModel.chatId = contact.id - } + ItemsModel.shared.loadOpenChat(contact.id) AlertManager.shared.showAlert(connReqSentAlert(.contact)) } } diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift index 1656710f55..4b43610236 100644 --- a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -56,7 +56,7 @@ struct ContactListNavLink: View { func recentContactNavLink(_ contact: Contact) -> some View { Button { dismissAllSheets(animated: true) { - ChatModel.shared.chatId = contact.id + ItemsModel.shared.loadOpenChat(contact.id) } } label: { contactPreview(contact, titleColor: theme.colors.onBackground) @@ -83,7 +83,7 @@ struct ContactListNavLink: View { Task { await MainActor.run { dismissAllSheets(animated: true) { - ChatModel.shared.chatId = contact.id + ItemsModel.shared.loadOpenChat(contact.id) } } } @@ -188,9 +188,7 @@ struct ContactListNavLink: View { Task { let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { alert = SomeAlert(alert: $0, id: "ContactListNavLink connectContactViaAddress") }) if ok { - await MainActor.run { - ChatModel.shared.chatId = contact.id - } + ItemsModel.shared.loadOpenChat(contact.id) DispatchQueue.main.async { dismissAllSheets(animated: true) { AlertManager.shared.showAlert(connReqSentAlert(.contact)) diff --git a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift index 2d5458b9d3..fdc3f2129f 100644 --- a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift +++ b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift @@ -7,16 +7,17 @@ // import SwiftUI +import SimpleXChat -struct NavLinkPlain: View { - @State var tag: V - @Binding var selection: V? +struct NavLinkPlain: View { + let chatId: ChatId + @Binding var selection: ChatId? @ViewBuilder var label: () -> Label var disabled = false var body: some View { ZStack { - Button("") { DispatchQueue.main.async { selection = tag } } + Button("") { ItemsModel.shared.loadOpenChat(chatId) } .disabled(disabled) label() } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 7e7944f984..113691abb3 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -37,7 +37,7 @@ struct AddGroupView: View { ) { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { dismissAllSheets(animated: true) { - m.chatId = groupInfo.id + ItemsModel.shared.loadOpenChat(groupInfo.id) } } } @@ -52,7 +52,7 @@ struct AddGroupView: View { ) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { dismissAllSheets(animated: true) { - m.chatId = groupInfo.id + ItemsModel.shared.loadOpenChat(groupInfo.id) } } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index cfe3dbb654..6cbc65e7c9 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -898,11 +898,11 @@ func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: DispatchQueue.main.async { if dismiss { dismissAllSheets(animated: true) { - m.chatId = c.id + ItemsModel.shared.loadOpenChat(c.id) showAlreadyExistsAlert?() } } else { - m.chatId = c.id + ItemsModel.shared.loadOpenChat(c.id) showAlreadyExistsAlert?() } } @@ -917,11 +917,11 @@ func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAler DispatchQueue.main.async { if dismiss { dismissAllSheets(animated: true) { - m.chatId = g.id + ItemsModel.shared.loadOpenChat(g.id) showAlreadyExistsAlert?() } } else { - m.chatId = g.id + ItemsModel.shared.loadOpenChat(g.id) showAlreadyExistsAlert?() } }