diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 9297aa7898..5ff6d39c96 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -320,15 +320,24 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] { let loadItemsPerPage = 50 -func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> Chat { +func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> (Chat, ChatGap?) { 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) } + if case let .apiChat(_, chat, gap) = r { return (Chat.init(chat), gap) } throw r } -func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] { +func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> ([ChatItem], ChatGap?) { let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search)) - if case let .apiChat(_, chat) = r { return chat.chatItems } + if case let .apiChat(_, chat, gap) = r { return (chat.chatItems, gap) } + if case .chatCmdError(_, _) = r { + if case .chatError(_, let chatError) = r { + if case .errorStore(let storeError) = chatError { + if case .chatItemNotFound(let itemId) = storeError { + itemNotFoundAlert() + } + } + } + } throw r } @@ -341,7 +350,7 @@ func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async { if clearItems { await MainActor.run { im.reversedChatItems = [] } } - let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) + 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) @@ -418,6 +427,13 @@ func apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage] return nil } +func itemNotFoundAlert() { + AlertManager.shared.showAlertMsg( + title: "Message no longer available", + message: "The quoted message you are trying to view has been deleted." + ) +} + private func sendMessageErrorAlert(_ r: ChatResponse) { logger.error("send message error: \(String(describing: r))") AlertManager.shared.showAlertMsg( diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 9b71e6c4a4..35ae1f9715 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -48,10 +48,18 @@ struct FramedItemView: View { if let qi = chatItem.quotedItem { ciQuoteView(qi) .onTapGesture { - if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) { - withAnimation { - scrollModel.scrollToItem(id: ci.id) + if let itemId = qi.itemId { + if !scrollToItem(itemId) { + Task { + if await loadItemsAround(chat.chatInfo, itemId) != nil { + await MainActor.run { + let _ = scrollToItem(itemId) + } + } + } } + } else { + itemNotFoundAlert() } } } else if let itemForwarded = chatItem.meta.itemForwarded { @@ -323,6 +331,42 @@ struct FramedItemView: View { return videoWidth } } + + // Scroll to an item, if success returns true otherwise false + private func scrollToItem(_ itemId: Int64) -> Bool { + if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == itemId }) { + withAnimation { + scrollModel.scrollToItem(id: ci.id) + } + return true + } else { + return false + } + } + + private func loadItemsAround(_ cInfo: ChatInfo, _ chatItemId: Int64) async -> [ChatItem]? { + do { + var reversedPage = Array() + let pagination: ChatPagination = .around(chatItemId: chatItemId, count: loadItemsPerPage * 2) + let (chatItems, _) = try await apiGetChatItems( + type: cInfo.chatType, + id: cInfo.apiId, + pagination: pagination, + search: "" + ) + + reversedPage.append(contentsOf: chatItems.reversed()) + + await MainActor.run { + ItemsModel.shared.reversedChatItems.append(contentsOf: reversedPage) + } + + return reversedPage + } catch let error { + logger.error("apiGetChat error: \(responseError(error))") + return nil + } + } } @ViewBuilder func toggleSecrets(_ ft: [FormattedText]?, _ showSecrets: Binding, _ v: V) -> some View { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 1acf08035c..e1ea89e250 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -854,7 +854,7 @@ struct ChatView: View { } else { .last(count: loadItemsPerPage) } - let chatItems = try await apiGetChatItems( + let (chatItems, _) = try await apiGetChatItems( type: cInfo.chatType, id: cInfo.apiId, pagination: pagination, diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index fd72b5b515..67a045ab9a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -340,7 +340,7 @@ struct GroupMemberInfoView: View { InfoViewButton(image: "message.fill", title: "message", width: width) { Task { do { - let chat = try await apiGetChat(type: .direct, id: contactId) + let (chat, _) = try await apiGetChat(type: .direct, id: contactId) chatModel.addChat(chat) ItemsModel.shared.loadOpenChat(chat.id) { dismissAllSheets(animated: true) diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 987f7f3d41..169078e1c7 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -214,7 +214,8 @@ public func chatResponse(_ s: String) -> ChatResponse { let user: UserRef = try? decodeObject(jApiChat["user"] as Any), let jChat = jApiChat["chat"] as? NSDictionary, let chat = try? parseChatData(jChat) { - return .apiChat(user: user, chat: chat) + let gap: ChatGap? = try? decodeObject(jApiChat["gap"] as Any) + return .apiChat(user: user, chat: chat, gap: gap) } } else if type == "chatCmdError" { if let jError = jResp["chatCmdError"] as? NSDictionary { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 3c9b91fa0b..3f44649f9e 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -546,7 +546,7 @@ public enum ChatResponse: Decodable, Error { case chatStopped case chatSuspended case apiChats(user: UserRef, chats: [ChatData]) - case apiChat(user: UserRef, chat: ChatData) + case apiChat(user: UserRef, chat: ChatData, gap: ChatGap?) case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) case userProtoServers(user: UserRef, servers: UserProtoServers) case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) @@ -888,7 +888,7 @@ public enum ChatResponse: Decodable, Error { case .chatStopped: return noDetails case .chatSuspended: return noDetails case let .apiChats(u, chats): return withUser(u, String(describing: chats)) - case let .apiChat(u, chat): return withUser(u, String(describing: chat)) + case let .apiChat(u, chat, gap): return withUser(u, "gap: \(String(describing: gap)) \(String(describing: chat))") case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))") case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") @@ -1133,12 +1133,16 @@ public enum ChatPagination { case last(count: Int) case after(chatItemId: Int64, count: Int) case before(chatItemId: Int64, count: Int) - + case around(chatItemId: Int64, count: Int) + case initial(count: Int) + var cmdString: String { switch self { case let .last(count): return "count=\(count)" case let .after(chatItemId, count): return "after=\(chatItemId) count=\(count)" case let .before(chatItemId, count): return "before=\(chatItemId) count=\(count)" + case let .around(chatItemId, count): return "around=\(chatItemId) count=\(count)" + case let .initial(count): return "initial=\(count)" } } } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1bd5673f01..b11a4127e6 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1529,6 +1529,11 @@ public struct ChatStats: Decodable, Hashable { public var unreadChat: Bool = false } +public struct ChatGap: Decodable, Hashable { + public var index: Int? + public var size: Int +} + public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var contactId: Int64 var localDisplayName: ContactName