diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/Shared/Model/ImageUtils.swift index 90070e74d..41d741e7e 100644 --- a/apps/ios/Shared/Model/ImageUtils.swift +++ b/apps/ios/Shared/Model/ImageUtils.swift @@ -195,18 +195,18 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? { } } -func generateNewFileName(_ prefix: String, _ ext: String) -> String { - uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)") +func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { + uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath) } -private func uniqueCombine(_ fileName: String) -> String { +private func uniqueCombine(_ fileName: String, fullPath: Bool = false) -> String { func tryCombine(_ fileName: String, _ n: Int) -> String { let ns = fileName as NSString let name = ns.deletingPathExtension let ext = ns.pathExtension let suffix = (n == 0) ? "" : "_\(n)" let f = "\(name)\(suffix).\(ext)" - return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f + return (FileManager.default.fileExists(atPath: fullPath ? f : getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f } return tryCombine(fileName, 0) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 057282177..4001edffb 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -384,10 +384,10 @@ struct ComposeView: View { } } .sheet(isPresented: $showMediaPicker) { - LibraryMediaListPicker(media: $chosenMedia, selectionLimit: 10) { itemsSelected in - showMediaPicker = false - if itemsSelected { - DispatchQueue.main.async { + LibraryMediaListPicker(addMedia: addMediaContent, selectionLimit: 10) { itemsSelected in + await MainActor.run { + showMediaPicker = false + if itemsSelected { composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: [])) } } @@ -488,6 +488,21 @@ struct ComposeView: View { } } + private func addMediaContent(_ content: UploadContent) async { + if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { + var newMedia: [(String, UploadContent?)] = [] + if case var .mediaPreviews(media) = composeState.preview { + media.append((img, content)) + newMedia = media + } else { + newMedia = [(img, content)] + } + await MainActor.run { + composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: newMedia)) + } + } + } + private var maxFileSize: Int64 { getMaxFileSize(.xftp) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 94a018749..09ead880a 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -16,7 +16,6 @@ struct GroupChatInfoView: View { @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat @Binding var groupInfo: GroupInfo - @ObservedObject private var alertManager = AlertManager.shared @State private var alert: GroupChatInfoViewAlert? = nil @State private var groupLink: String? @State private var groupLinkMemberRole: GroupMemberRole = .member diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 4a187cecb..7e336c332 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -188,17 +188,19 @@ struct GroupMemberInfoView: View { // this condition prevents re-setting picker if !justOpened { return } } - newRole = member.memberRole - do { - let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) - let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) - _ = chatModel.upsertGroupMember(groupInfo, mem) - connectionStats = stats - connectionCode = code - } catch let error { - logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") - } justOpened = false + DispatchQueue.main.async { + newRole = member.memberRole + do { + let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) + let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) + _ = chatModel.upsertGroupMember(groupInfo, mem) + connectionStats = stats + connectionCode = code + } catch let error { + logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") + } + } } .onChange(of: newRole) { newRole in if newRole != member.memberRole { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 7e123c389..18cc3f4d8 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -103,8 +103,10 @@ struct GroupProfileView: View { } } .sheet(isPresented: $showImagePicker) { - LibraryImagePicker(image: $chosenImage) { - didSelectItem in showImagePicker = false + LibraryImagePicker(image: $chosenImage) { _ in + await MainActor.run { + showImagePicker = false + } } } .onChange(of: chosenImage) { image in diff --git a/apps/ios/Shared/Views/Chat/ScanCodeView.swift b/apps/ios/Shared/Views/Chat/ScanCodeView.swift index 09861fa50..f364b4ed0 100644 --- a/apps/ios/Shared/Views/Chat/ScanCodeView.swift +++ b/apps/ios/Shared/Views/Chat/ScanCodeView.swift @@ -17,7 +17,7 @@ struct ScanCodeView: View { var body: some View { VStack(alignment: .leading) { - CodeScannerView(codeTypes: [.qr], completion: processQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) Text("Scan security code from your contact's app.") diff --git a/apps/ios/Shared/Views/Helpers/ImagePicker.swift b/apps/ios/Shared/Views/Helpers/ImagePicker.swift index 1b44c2313..0e3f8082b 100644 --- a/apps/ios/Shared/Views/Helpers/ImagePicker.swift +++ b/apps/ios/Shared/Views/Helpers/ImagePicker.swift @@ -13,112 +13,122 @@ import SimpleXChat struct LibraryImagePicker: View { @Binding var image: UIImage? - var didFinishPicking: (_ didSelectItems: Bool) -> Void - @State var images: [UploadContent] = [] + var didFinishPicking: (_ didSelectImage: Bool) async -> Void + @State var mediaAdded = false var body: some View { - LibraryMediaListPicker(media: $images, selectionLimit: 1, didFinishPicking: didFinishPicking) - .onChange(of: images) { _ in - if let img = images.first { - image = img.uiImage - } - } + LibraryMediaListPicker(addMedia: addMedia, selectionLimit: 1, didFinishPicking: didFinishPicking) + } + + private func addMedia(_ content: UploadContent) async { + if mediaAdded { return } + await MainActor.run { + mediaAdded = true + image = content.uiImage + } } } struct LibraryMediaListPicker: UIViewControllerRepresentable { typealias UIViewControllerType = PHPickerViewController - @Binding var media: [UploadContent] + var addMedia: (_ content: UploadContent) async -> Void var selectionLimit: Int - var didFinishPicking: (_ didSelectItems: Bool) -> Void + var didFinishPicking: (_ didSelectItems: Bool) async -> Void class Coordinator: PHPickerViewControllerDelegate { let parent: LibraryMediaListPicker let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryMediaListPicker") - var media: [UploadContent] = [] - var mediaCount: Int = 0 init(_ parent: LibraryMediaListPicker) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - parent.didFinishPicking(!results.isEmpty) - guard !results.isEmpty else { - return + Task { + await parent.didFinishPicking(!results.isEmpty) + if results.isEmpty { return } + for r in results { + await loadItem(r.itemProvider) + } } + } - parent.media = [] - media = [] - mediaCount = results.count - for result in results { - logger.log("LibraryMediaListPicker result") - let p = result.itemProvider - if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { - p.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in - if let url = url { - let tempUrl = URL(fileURLWithPath: getTempFilesDirectory().path + "/" + generateNewFileName("video", url.pathExtension)) - if ((try? FileManager.default.copyItem(at: url, to: tempUrl)) != nil) { - ChatModel.shared.filesToDelete.insert(tempUrl) - self.loadVideo(url: tempUrl, error: error) + private func loadItem(_ p: NSItemProvider) async { + logger.debug("LibraryMediaListPicker result") + if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { + if let video = await loadVideo(p) { + await self.parent.addMedia(video) + logger.debug("LibraryMediaListPicker: added video") + } + } else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) { + if let img = await loadImageData(p) { + await self.parent.addMedia(img) + logger.debug("LibraryMediaListPicker: added image") + } + } else if p.canLoadObject(ofClass: UIImage.self) { + if let img = await loadImage(p) { + await self.parent.addMedia(.simpleImage(image: img)) + logger.debug("LibraryMediaListPicker: added image") + } + } + } + + private func loadImageData(_ p: NSItemProvider) async -> UploadContent? { + await withCheckedContinuation { cont in + loadFileURL(p, type: UTType.data) { url in + if let url = url { + let img = UploadContent.loadFromURL(url: url) + cont.resume(returning: img) + } else { + cont.resume(returning: nil) + } + } + } + } + + private func loadImage(_ p: NSItemProvider) async -> UIImage? { + await withCheckedContinuation { cont in + p.loadObject(ofClass: UIImage.self) { obj, err in + if let err = err { + logger.error("LibraryMediaListPicker result image error: \(err.localizedDescription)") + cont.resume(returning: nil) + } else { + cont.resume(returning: obj as? UIImage) + } + } + } + } + + private func loadVideo(_ p: NSItemProvider) async -> UploadContent? { + await withCheckedContinuation { cont in + loadFileURL(p, type: UTType.movie) { url in + if let url = url { + let tempUrl = URL(fileURLWithPath: generateNewFileName(getTempFilesDirectory().path + "/" + "video", url.pathExtension, fullPath: true)) + do { +// logger.debug("LibraryMediaListPicker copyItem \(url) to \(tempUrl)") + try FileManager.default.copyItem(at: url, to: tempUrl) + DispatchQueue.main.async { + _ = ChatModel.shared.filesToDelete.insert(tempUrl) } + let video = UploadContent.loadVideoFromURL(url: tempUrl) + cont.resume(returning: video) + return + } catch let err { + logger.error("LibraryMediaListPicker copyItem error: \(err.localizedDescription)") } } - } else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) { - p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in - self.loadImage(object: url, error: error) - } - } else if p.canLoadObject(ofClass: UIImage.self) { - p.loadObject(ofClass: UIImage.self) { image, error in - DispatchQueue.main.async { - self.loadImage(object: image, error: error) - } - } + cont.resume(returning: nil) + } + } + } + + private func loadFileURL(_ p: NSItemProvider, type: UTType, completion: @escaping (URL?) -> Void) { + p.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, err in + if let err = err { + logger.error("LibraryMediaListPicker loadFileURL error: \(err.localizedDescription)") + completion(nil) } else { - dispatchQueue.sync { self.mediaCount -= 1} - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { - self.dispatchQueue.sync { - if self.parent.media.count == 0 { - logger.log("LibraryMediaListPicker: added \(self.media.count) images out of \(results.count)") - self.parent.media = self.media - } - } - } - } - - func loadImage(object: Any?, error: Error? = nil) { - if let error = error { - logger.error("LibraryMediaListPicker: couldn't load image with error: \(error.localizedDescription)") - } else if let image = object as? UIImage { - media.append(.simpleImage(image: image)) - logger.log("LibraryMediaListPicker: added image") - } else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) { - media.append(image) - } - dispatchQueue.sync { - self.mediaCount -= 1 - if self.mediaCount == 0 && self.parent.media.count == 0 { - logger.log("LibraryMediaListPicker: added all media") - self.parent.media = self.media - self.media = [] - } - } - } - - func loadVideo(url: URL?, error: Error? = nil) { - if let error = error { - logger.error("LibraryMediaListPicker: couldn't load video with error: \(error.localizedDescription)") - } else if let url = url as URL?, let video = UploadContent.loadVideoFromURL(url: url) { - media.append(video) - } - dispatchQueue.sync { - self.mediaCount -= 1 - if self.mediaCount == 0 && self.parent.media.count == 0 { - logger.log("LibraryMediaListPicker: added all media") - self.parent.media = self.media - self.media = [] + completion(url) } } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 2d7f31c58..6c7919669 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -130,8 +130,10 @@ struct AddGroupView: View { } } .sheet(isPresented: $showImagePicker) { - LibraryImagePicker(image: $chosenImage) { - didSelectItem in showImagePicker = false + LibraryImagePicker(image: $chosenImage) { _ in + await MainActor.run { + showImagePicker = false + } } } .alert(isPresented: $showInvalidNameAlert) { diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index 82c4629c0..3ddb85079 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -74,6 +74,7 @@ struct QRCode: View { .onAppear { image = image ?? generateImage(uri, tintColor: tintColor) } + .frame(maxWidth: .infinity, maxHeight: .infinity) } } diff --git a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift index 9a11eee92..7f3f5e02f 100644 --- a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift +++ b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift @@ -25,7 +25,7 @@ struct ScanToConnectView: View { .fixedSize(horizontal: false, vertical: true) .padding(.vertical) - CodeScannerView(codeTypes: [.qr], completion: processQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index e934bbc89..6809dc138 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -332,7 +332,7 @@ struct ConnectDesktopView: View { private func scanDesctopAddressView() -> some View { Section("Scan QR code from desktop") { - CodeScannerView(codeTypes: [.qr], completion: processDesktopQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) .listRowBackground(Color.clear) diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift index 8e8885b51..9da3bac00 100644 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift @@ -51,9 +51,9 @@ struct AdvancedNetworkSettings: View { } .disabled(currentNetCfg == NetCfg.proxyDefaults) - timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel) - timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [3_000000, 5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) - timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 60_000, 90_000, 120_000], label: secondsLabel) + timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 45_000, 60_000, 90_000, 120_000], label: secondsLabel) timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "") Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive) diff --git a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift index ffdbd1b07..33825fee6 100644 --- a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift +++ b/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift @@ -21,7 +21,7 @@ struct ScanProtocolServer: View { .font(.largeTitle) .bold() .padding(.vertical) - CodeScannerView(codeTypes: [.qr], completion: processQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) .padding(.top) diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index b64ec21de..e5ec23178 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -120,8 +120,10 @@ struct UserProfile: View { } } .sheet(isPresented: $showImagePicker) { - LibraryImagePicker(image: $chosenImage) { - didSelectItem in showImagePicker = false + LibraryImagePicker(image: $chosenImage) { _ in + await MainActor.run { + showImagePicker = false + } } } .onChange(of: chosenImage) { image in diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 01696ad9e..42f76fd48 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -63,11 +63,6 @@ 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; - 5C8EA13D2B25206A001DE5E4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA1382B25206A001DE5E4 /* libgmp.a */; }; - 5C8EA13E2B25206A001DE5E4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA1392B25206A001DE5E4 /* libgmpxx.a */; }; - 5C8EA13F2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA13A2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a */; }; - 5C8EA1402B25206A001DE5E4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA13B2B25206A001DE5E4 /* libffi.a */; }; - 5C8EA1412B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C8EA13C2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a */; }; 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; }; 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; }; @@ -121,6 +116,11 @@ 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; + 5CCD1A602B27927E001A4199 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */; }; + 5CCD1A612B27927E001A4199 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5C2B27927E001A4199 /* libgmp.a */; }; + 5CCD1A622B27927E001A4199 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5D2B27927E001A4199 /* libffi.a */; }; + 5CCD1A632B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */; }; + 5CCD1A642B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -338,11 +338,6 @@ 5C8B41C929AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 5C8B41CB29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = "cs.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C8B41CC29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; - 5C8EA1382B25206A001DE5E4 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C8EA1392B25206A001DE5E4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C8EA13A2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a"; sourceTree = ""; }; - 5C8EA13B2B25206A001DE5E4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C8EA13C2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a"; sourceTree = ""; }; 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServersView.swift; sourceTree = ""; }; 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServerView.swift; sourceTree = ""; }; 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecPlay.swift; sourceTree = ""; }; @@ -412,6 +407,11 @@ 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; + 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CCD1A5C2B27927E001A4199 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CCD1A5D2B27927E001A4199 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a"; sourceTree = ""; }; + 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a"; sourceTree = ""; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; @@ -527,13 +527,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C8EA13F2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a in Frameworks */, - 5C8EA1402B25206A001DE5E4 /* libffi.a in Frameworks */, - 5C8EA13E2B25206A001DE5E4 /* libgmpxx.a in Frameworks */, + 5CCD1A602B27927E001A4199 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C8EA13D2B25206A001DE5E4 /* libgmp.a in Frameworks */, - 5C8EA1412B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a in Frameworks */, + 5CCD1A612B27927E001A4199 /* libgmp.a in Frameworks */, + 5CCD1A622B27927E001A4199 /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + 5CCD1A642B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */, + 5CCD1A632B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -595,11 +595,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C8EA13B2B25206A001DE5E4 /* libffi.a */, - 5C8EA1382B25206A001DE5E4 /* libgmp.a */, - 5C8EA1392B25206A001DE5E4 /* libgmpxx.a */, - 5C8EA13A2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX-ghc9.6.3.a */, - 5C8EA13C2B25206A001DE5E4 /* libHSsimplex-chat-5.4.0.7-1uCDT6bmj7t4ctyD1vFaZX.a */, + 5CCD1A5D2B27927E001A4199 /* libffi.a */, + 5CCD1A5C2B27927E001A4199 /* libgmp.a */, + 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */, + 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */, + 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 4d1446965..a199966ba 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1207,9 +1207,9 @@ public struct NetCfg: Codable, Equatable { public static let defaults: NetCfg = NetCfg( socksProxy: nil, sessionMode: TransportSessionMode.user, - tcpConnectTimeout: 15_000_000, - tcpTimeout: 10_000_000, - tcpTimeoutPerKb: 30_000, + tcpConnectTimeout: 20_000_000, + tcpTimeout: 15_000_000, + tcpTimeoutPerKb: 45_000, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 6c01aff5d..ad897c60f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2800,9 +2800,9 @@ data class NetCfg( hostMode = HostMode.OnionViaSocks, requiredHostMode = false, sessionMode = TransportSessionMode.User, - tcpConnectTimeout = 15_000_000, - tcpTimeout = 10_000_000, - tcpTimeoutPerKb = 30_000, + tcpConnectTimeout = 20_000_000, + tcpTimeout = 15_000_000, + tcpTimeoutPerKb = 45_000, tcpKeepAlive = KeepAliveOpts.defaults, smpPingInterval = 1200_000_000, smpPingCount = 3 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt index 584917820..5fb8bfb03 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt @@ -154,20 +154,20 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { SectionItemView { TimeoutSettingRow( stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, - listOf(5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000_000, 45_000_000), secondsLabel + listOf(7_500000, 10_000000, 15_000000, 20_000000, 30_000_000, 45_000_000), secondsLabel ) } SectionItemView { TimeoutSettingRow( stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeout, - listOf(3_000000, 5_000000, 7_000000, 10_000000, 15_000000, 20_000_000, 30_000_000), secondsLabel + listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000, 30_000_000), secondsLabel ) } SectionItemView { // can't be higher than 130ms to avoid overflow on 32bit systems TimeoutSettingRow( stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb, - listOf(15_000, 30_000, 60_000, 90_000, 120_000), secondsLabel + listOf(15_000, 30_000, 45_000, 60_000, 90_000, 120_000), secondsLabel ) } SectionItemView { diff --git a/cabal.project b/cabal.project index b0dbaab3e..699783023 100644 --- a/cabal.project +++ b/cabal.project @@ -4,25 +4,23 @@ packages: . with-compiler: ghc-8.10.7 -index-state: 2023-10-06T00:00:00Z +index-state: 2023-12-12T00:00:00Z + +package cryptostore + flags: +use_crypton constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 560dc553127851fa1fb201d0a9c80dcf1ad6e5dc + tag: 18be2709f59a4cb20fe9758b899622092dba062e source-repository-package type: git location: https://github.com/simplex-chat/hs-socks.git tag: a30cc7a79a08d8108316094f8f2f82a0c5e1ac51 -source-repository-package - type: git - location: https://github.com/kazu-yamamoto/http2.git - tag: f5525b755ff2418e6e6ecc69e877363b0d0bcaeb - source-repository-package type: git location: https://github.com/simplex-chat/direct-sqlcipher.git diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index a43a69409..5362e4f2c 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -7,7 +7,7 @@ revision: 25.11.2023 | Updated 25.11.2023 | Languages: EN | # Download SimpleX apps -The latest stable version is v5.4.0. +The latest stable version is v5.4.1. You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). @@ -21,24 +21,24 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch Using the same profile as on mobile device is not yet supported – you need to create a separate profile to use desktop apps. -**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-desktop-ubuntu-22_04-x86_64.deb). +**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-ubuntu-22_04-x86_64.deb). -**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). +**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). -**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-desktop-windows-x86_64.msi). +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-desktop-windows-x86_64.msi). ## Mobile apps **iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu). -**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-armv7a.apk). +**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-armv7a.apk). ## Terminal (console) app See [Using terminal app](/docs/CLI.md). -**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-chat-ubuntu-22_04-x86-64). +**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-ubuntu-22_04-x86-64). -**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#). +**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#). -**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.0/simplex-chat-windows-x86-64). +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.4.1/simplex-chat-windows-x86-64). diff --git a/flake.lock b/flake.lock index 81b3f6f6b..355caaf57 100644 --- a/flake.lock +++ b/flake.lock @@ -288,11 +288,11 @@ "hackage": { "flake": false, "locked": { - "lastModified": 1696724662, - "narHash": "sha256-jV2ugSjZE0FjMYR2YIx0p2cDBqd+xxhZrRxp5BmieYk=", + "lastModified": 1702340598, + "narHash": "sha256-CC0HI+6iKPtH+8r/ZfcpW5v/OYvL7zMwpr0xfkXV1zU=", "owner": "input-output-hk", "repo": "hackage.nix", - "rev": "df603bff8606d8653a0876ae0c3fd1f9014882f2", + "rev": "24617c569995e38bf3b83b48eec6628a50fdb4fb", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 1bcde2c85..24a5062be 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ let pkgs = haskellNix.legacyPackages.${system}.appendOverlays [android26]; in let drv' = { extra-modules, pkgs', ... }: pkgs'.haskell-nix.project { compiler-nix-name = "ghc8107"; - index-state = "2023-10-06T00:00:00Z"; + index-state = "2023-12-12T00:00:00Z"; # We need this, to specify we want the cabal project. # If the stack.yaml was dropped, this would not be necessary. projectFileName = "cabal.project"; diff --git a/package.yaml b/package.yaml index 0f27f6035..4c2d7b3af 100644 --- a/package.yaml +++ b/package.yaml @@ -22,7 +22,7 @@ dependencies: - composition == 1.0.* - constraints >= 0.12 && < 0.14 - containers == 0.6.* - - cryptonite == 0.30.* + - crypton == 0.34.* - data-default >= 0.7 && < 0.8 - directory == 1.3.* - direct-sqlcipher == 2.3.* @@ -46,7 +46,7 @@ dependencies: - stm == 2.5.* - terminal == 0.2.* - time == 1.9.* - - tls >= 1.6.0 && < 1.7 + - tls >= 1.7.0 && < 1.8 - unliftio == 0.2.* - unliftio-core == 0.2.* - zip == 2.0.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index f4953a57a..3733163f4 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,8 +1,7 @@ { - "https://github.com/simplex-chat/simplexmq.git"."560dc553127851fa1fb201d0a9c80dcf1ad6e5dc" = "1xz3lw5dsh7gm136jzwmsbqjigsqsnjlbhg38mpc6lm586lg8f9x"; + "https://github.com/simplex-chat/simplexmq.git"."18be2709f59a4cb20fe9758b899622092dba062e" = "08dr4vyg1wz2z768iikg8fks5zqf4dw5myr87hbpv964idda3pmj"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; - "https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6"; - "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; + "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7f8b844e2..a9ddfaf05 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -175,7 +175,7 @@ library , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite ==0.30.* + , crypton ==0.34.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -199,7 +199,7 @@ library , stm ==2.5.* , terminal ==0.2.* , time ==1.9.* - , tls >=1.6.0 && <1.7 + , tls >=1.7.0 && <1.8 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -234,7 +234,7 @@ executable simplex-bot , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite ==0.30.* + , crypton ==0.34.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -259,7 +259,7 @@ executable simplex-bot , stm ==2.5.* , terminal ==0.2.* , time ==1.9.* - , tls >=1.6.0 && <1.7 + , tls >=1.7.0 && <1.8 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -294,7 +294,7 @@ executable simplex-bot-advanced , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite ==0.30.* + , crypton ==0.34.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -319,7 +319,7 @@ executable simplex-bot-advanced , stm ==2.5.* , terminal ==0.2.* , time ==1.9.* - , tls >=1.6.0 && <1.7 + , tls >=1.7.0 && <1.8 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -356,7 +356,7 @@ executable simplex-broadcast-bot , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite ==0.30.* + , crypton ==0.34.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -381,7 +381,7 @@ executable simplex-broadcast-bot , stm ==2.5.* , terminal ==0.2.* , time ==1.9.* - , tls >=1.6.0 && <1.7 + , tls >=1.7.0 && <1.8 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -417,7 +417,7 @@ executable simplex-chat , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite ==0.30.* + , crypton ==0.34.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -442,7 +442,7 @@ executable simplex-chat , stm ==2.5.* , terminal ==0.2.* , time ==1.9.* - , tls >=1.6.0 && <1.7 + , tls >=1.7.0 && <1.8 , unliftio ==0.2.* , unliftio-core ==0.2.* , websockets ==0.12.* @@ -482,7 +482,7 @@ executable simplex-directory-service , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite ==0.30.* + , crypton ==0.34.* , data-default ==0.7.* , direct-sqlcipher ==2.3.* , directory ==1.3.* @@ -507,7 +507,7 @@ executable simplex-directory-service , stm ==2.5.* , terminal ==0.2.* , time ==1.9.* - , tls >=1.6.0 && <1.7 + , tls >=1.7.0 && <1.8 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -571,7 +571,7 @@ test-suite simplex-chat-test , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* - , cryptonite ==0.30.* + , crypton ==0.34.* , data-default ==0.7.* , deepseq ==1.4.* , direct-sqlcipher ==2.3.* @@ -600,7 +600,7 @@ test-suite simplex-chat-test , stm ==2.5.* , terminal ==0.2.* , time ==1.9.* - , tls >=1.6.0 && <1.7 + , tls >=1.7.0 && <1.8 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* diff --git a/src/Simplex/Chat/Migrations/M20231207_chat_list_pagination.hs b/src/Simplex/Chat/Migrations/M20231207_chat_list_pagination.hs index cf272ae65..9a8944c5c 100644 --- a/src/Simplex/Chat/Migrations/M20231207_chat_list_pagination.hs +++ b/src/Simplex/Chat/Migrations/M20231207_chat_list_pagination.hs @@ -26,9 +26,6 @@ CREATE INDEX idx_contacts_chat_ts ON contacts(user_id, chat_ts); CREATE INDEX idx_groups_chat_ts ON groups(user_id, chat_ts); CREATE INDEX idx_contact_requests_updated_at ON contact_requests(user_id, updated_at); CREATE INDEX idx_connections_updated_at ON connections(user_id, updated_at); - -CREATE INDEX idx_chat_items_contact_id_item_status ON chat_items(contact_id, item_status); -CREATE INDEX idx_chat_items_group_id_item_status ON chat_items(group_id, item_status); |] down_m20231207_chat_list_pagination :: Query @@ -38,7 +35,4 @@ DROP INDEX idx_contacts_chat_ts; DROP INDEX idx_groups_chat_ts; DROP INDEX idx_contact_requests_updated_at; DROP INDEX idx_connections_updated_at; - -DROP INDEX idx_chat_items_contact_id_item_status; -DROP INDEX idx_chat_items_group_id_item_status; |] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index ab431f84d..3b83b132d 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -817,11 +817,3 @@ CREATE INDEX idx_contact_requests_updated_at ON contact_requests( updated_at ); CREATE INDEX idx_connections_updated_at ON connections(user_id, updated_at); -CREATE INDEX idx_chat_items_contact_id_item_status ON chat_items( - contact_id, - item_status -); -CREATE INDEX idx_chat_items_group_id_item_status ON chat_items( - group_id, - item_status -); diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 9986eacf6..87e666712 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -499,8 +499,8 @@ getChatPreviews db user withPCC pagination query = do where ts :: AChatPreviewData -> UTCTime ts (ACPD _ cpd) = case cpd of - (DirectChatPD t _ _) -> t - (GroupChatPD t _ _) -> t + (DirectChatPD t _ _ _) -> t + (GroupChatPD t _ _ _) -> t (ContactRequestPD t _) -> t (ContactConnectionPD t _) -> t sortTake = case pagination of @@ -515,8 +515,8 @@ getChatPreviews db user withPCC pagination query = do SCTContactConnection -> let (ContactConnectionPD _ chat) = cpd in pure chat data ChatPreviewData (c :: ChatType) where - DirectChatPD :: UTCTime -> ContactId -> Maybe ChatStats -> ChatPreviewData 'CTDirect - GroupChatPD :: UTCTime -> GroupId -> Maybe ChatStats -> ChatPreviewData 'CTGroup + DirectChatPD :: UTCTime -> ContactId -> Maybe ChatItemId -> ChatStats -> ChatPreviewData 'CTDirect + GroupChatPD :: UTCTime -> GroupId -> Maybe ChatItemId -> ChatStats -> ChatPreviewData 'CTGroup ContactRequestPD :: UTCTime -> AChat -> ChatPreviewData 'CTContactRequest ContactConnectionPD :: UTCTime -> AChat -> ChatPreviewData 'CTContactConnection @@ -528,283 +528,200 @@ paginationByTimeFilter = \case PTAfter ts count -> ("\nAND ts > :ts ORDER BY ts ASC LIMIT :count", [":ts" := ts, ":count" := count]) PTBefore ts count -> ("\nAND ts < :ts ORDER BY ts DESC LIMIT :count", [":ts" := ts, ":count" := count]) -type MaybeChatStatsRow = (Maybe Int, Maybe ChatItemId, Maybe Bool) +type ChatStatsRow = (Int, ChatItemId, Bool) -toMaybeChatStats :: MaybeChatStatsRow -> Maybe ChatStats -toMaybeChatStats (Just unreadCount, Just minUnreadItemId, Just unreadChat) = Just ChatStats {unreadCount, minUnreadItemId, unreadChat} -toMaybeChatStats _ = Nothing +toChatStats :: ChatStatsRow -> ChatStats +toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat} findDirectChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] findDirectChatPreviews_ db User {userId} pagination clq = map toPreview <$> getPreviews where - toPreview :: (ContactId, UTCTime) :. MaybeChatStatsRow -> AChatPreviewData - toPreview ((contactId, ts) :. statsRow_) = - ACPD SCTDirect $ DirectChatPD ts contactId (toMaybeChatStats statsRow_) + toPreview :: (ContactId, UTCTime, Maybe ChatItemId) :. ChatStatsRow -> AChatPreviewData + toPreview ((contactId, ts, lastItemId_) :. statsRow) = + ACPD SCTDirect $ DirectChatPD ts contactId lastItemId_ (toChatStats statsRow) + baseQuery = + [sql| + SELECT ct.contact_id, ct.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat + FROM contacts ct + LEFT JOIN ( + SELECT contact_id, chat_item_id, MAX(created_at) + FROM chat_items + GROUP BY contact_id + ) LastItems ON LastItems.contact_id = ct.contact_id + LEFT JOIN ( + SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE item_status = :rcv_new + GROUP BY contact_id + ) ChatStats ON ChatStats.contact_id = ct.contact_id + |] (pagQuery, pagParams) = paginationByTimeFilter pagination getPreviews = case clq of CLQFilters {favorite = False, unread = False} -> DB.queryNamed db - ( [sql| - SELECT ct.contact_id, ct.chat_ts as ts, NULL, NULL, NULL - FROM contacts ct - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - |] + ( baseQuery + <> [sql| + WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + |] <> pagQuery ) - ([":user_id" := userId] <> pagParams) + ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQFilters {favorite = True, unread = False} -> DB.queryNamed db - ( [sql| - SELECT ct.contact_id, ct.chat_ts as ts, NULL, NULL, NULL - FROM contacts ct - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND ct.favorite = 1 - |] + ( baseQuery + <> [sql| + WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + AND ct.favorite = 1 + |] <> pagQuery ) - ([":user_id" := userId] <> pagParams) + ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQFilters {favorite = False, unread = True} -> DB.queryNamed db - ( [sql| - SELECT ct.contact_id, ct.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat - FROM contacts ct - LEFT JOIN ( - SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread - FROM chat_items - WHERE item_status = :rcv_new - GROUP BY contact_id - ) ChatStats ON ChatStats.contact_id = ct.contact_id - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] + ( baseQuery + <> [sql| + WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + AND (ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] <> pagQuery ) ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQFilters {favorite = True, unread = True} -> DB.queryNamed db - ( [sql| - SELECT ct.contact_id, ct.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat - FROM contacts ct - LEFT JOIN ( - SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread - FROM chat_items - WHERE item_status = :rcv_new - GROUP BY contact_id - ) ChatStats ON ChatStats.contact_id = ct.contact_id - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND (ct.favorite = 1 - OR ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] + ( baseQuery + <> [sql| + WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + AND (ct.favorite = 1 + OR ct.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] <> pagQuery ) ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQSearch {search} -> DB.queryNamed db - ( [sql| - SELECT ct.contact_id, ct.chat_ts as ts, NULL, NULL, NULL - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used - AND ( - ct.local_display_name LIKE '%' || :search || '%' - OR cp.display_name LIKE '%' || :search || '%' - OR cp.full_name LIKE '%' || :search || '%' - OR cp.local_alias LIKE '%' || :search || '%' - ) - |] + ( baseQuery + <> [sql| + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + WHERE ct.user_id = :user_id AND ct.is_user = 0 AND ct.deleted = 0 AND ct.contact_used + AND ( + ct.local_display_name LIKE '%' || :search || '%' + OR cp.display_name LIKE '%' || :search || '%' + OR cp.full_name LIKE '%' || :search || '%' + OR cp.local_alias LIKE '%' || :search || '%' + ) + |] <> pagQuery ) - ([":user_id" := userId, ":search" := search] <> pagParams) + ([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) getDirectChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat -getDirectChatPreview_ db user (DirectChatPD _ contactId stats_) = do +getDirectChatPreview_ db user (DirectChatPD _ contactId lastItemId_ stats) = do contact <- getContact db user contactId - lastItem <- getLastItem - stats <- maybe getChatStats pure stats_ + lastItem <- case lastItemId_ of + Just lastItemId -> (: []) <$> getDirectChatItem db user contactId lastItemId + Nothing -> pure [] pure $ AChat SCTDirect (Chat (DirectChat contact) lastItem stats) - where - getLastItem :: ExceptT StoreError IO [CChatItem 'CTDirect] - getLastItem = - liftIO getLastItemId >>= \case - Nothing -> pure [] - Just lastItemId -> (: []) <$> getDirectChatItem db user contactId lastItemId - getLastItemId :: IO (Maybe ChatItemId) - getLastItemId = - maybeFirstRow fromOnly $ - DB.query - db - [sql| - SELECT chat_item_id FROM ( - SELECT contact_id, chat_item_id, MAX(created_at) - FROM chat_items - WHERE contact_id = ? - GROUP BY contact_id - ) - |] - (Only contactId) - getChatStats :: ExceptT StoreError IO ChatStats - getChatStats = do - r_ <- liftIO getUnreadStats - let (unreadCount, minUnreadItemId) = maybe (0, 0) (\(_, unreadCnt, minId) -> (unreadCnt, minId)) r_ - -- unread_chat could be read into contact to not search twice - unreadChat <- - ExceptT . firstRow fromOnly (SEInternalError $ "unread_chat not found for contact " <> show contactId) $ - DB.query db "SELECT unread_chat FROM contacts WHERE contact_id = ?" (Only contactId) - pure ChatStats {unreadCount, minUnreadItemId, unreadChat} - getUnreadStats :: IO (Maybe (ContactId, Int, ChatItemId)) - getUnreadStats = - maybeFirstRow id $ - DB.query - db - [sql| - SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread - FROM chat_items - WHERE contact_id = ? AND item_status = ? - GROUP BY contact_id - |] - (contactId, CISRcvNew) findGroupChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] findGroupChatPreviews_ db User {userId} pagination clq = map toPreview <$> getPreviews where - toPreview :: (GroupId, UTCTime) :. MaybeChatStatsRow -> AChatPreviewData - toPreview ((groupId, ts) :. statsRow_) = - ACPD SCTGroup $ GroupChatPD ts groupId (toMaybeChatStats statsRow_) + toPreview :: (GroupId, UTCTime, Maybe ChatItemId) :. ChatStatsRow -> AChatPreviewData + toPreview ((groupId, ts, lastItemId_) :. statsRow) = + ACPD SCTGroup $ GroupChatPD ts groupId lastItemId_ (toChatStats statsRow) + baseQuery = + [sql| + SELECT g.group_id, g.chat_ts as ts, LastItems.chat_item_id, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat + FROM groups g + LEFT JOIN ( + SELECT group_id, chat_item_id, MAX(item_ts) + FROM chat_items + GROUP BY group_id + ) LastItems ON LastItems.group_id = g.group_id + LEFT JOIN ( + SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread + FROM chat_items + WHERE item_status = :rcv_new + GROUP BY group_id + ) ChatStats ON ChatStats.group_id = g.group_id + |] (pagQuery, pagParams) = paginationByTimeFilter pagination getPreviews = case clq of CLQFilters {favorite = False, unread = False} -> DB.queryNamed db - ( [sql| - SELECT g.group_id, g.chat_ts as ts, NULL, NULL, NULL - FROM groups g - WHERE g.user_id = :user_id - |] + ( baseQuery + <> [sql| + WHERE g.user_id = :user_id + |] <> pagQuery ) - ([":user_id" := userId] <> pagParams) + ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQFilters {favorite = True, unread = False} -> DB.queryNamed db - ( [sql| - SELECT g.group_id, g.chat_ts as ts, NULL, NULL, NULL - FROM groups g - WHERE g.user_id = :user_id - AND g.favorite = 1 - |] + ( baseQuery + <> [sql| + WHERE g.user_id = :user_id + AND g.favorite = 1 + |] <> pagQuery ) - ([":user_id" := userId] <> pagParams) + ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQFilters {favorite = False, unread = True} -> DB.queryNamed db - ( [sql| - SELECT g.group_id, g.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat - FROM groups g - LEFT JOIN ( - SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread - FROM chat_items - WHERE item_status = :rcv_new - GROUP BY group_id - ) ChatStats ON ChatStats.group_id = g.group_id - WHERE g.user_id = :user_id - AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] + ( baseQuery + <> [sql| + WHERE g.user_id = :user_id + AND (g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] <> pagQuery ) ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQFilters {favorite = True, unread = True} -> DB.queryNamed db - ( [sql| - SELECT g.group_id, g.chat_ts as ts, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), g.unread_chat - FROM groups g - LEFT JOIN ( - SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread - FROM chat_items - WHERE item_status = :rcv_new - GROUP BY group_id - ) ChatStats ON ChatStats.group_id = g.group_id - WHERE g.user_id = :user_id - AND (g.favorite = 1 - OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) - |] + ( baseQuery + <> [sql| + WHERE g.user_id = :user_id + AND (g.favorite = 1 + OR g.unread_chat = 1 OR ChatStats.UnreadCount > 0) + |] <> pagQuery ) ([":user_id" := userId, ":rcv_new" := CISRcvNew] <> pagParams) CLQSearch {search} -> DB.queryNamed db - ( [sql| - SELECT g.group_id, g.chat_ts as ts, NULL, NULL, NULL - FROM groups g - JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - WHERE g.user_id = :user_id - AND ( - g.local_display_name LIKE '%' || :search || '%' - OR gp.display_name LIKE '%' || :search || '%' - OR gp.full_name LIKE '%' || :search || '%' - OR gp.description LIKE '%' || :search || '%' - ) - |] + ( baseQuery + <> [sql| + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = :user_id + AND ( + g.local_display_name LIKE '%' || :search || '%' + OR gp.display_name LIKE '%' || :search || '%' + OR gp.full_name LIKE '%' || :search || '%' + OR gp.description LIKE '%' || :search || '%' + ) + |] <> pagQuery ) - ([":user_id" := userId, ":search" := search] <> pagParams) + ([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) getGroupChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat -getGroupChatPreview_ db user (GroupChatPD _ groupId stats_) = do +getGroupChatPreview_ db user (GroupChatPD _ groupId lastItemId_ stats) = do groupInfo <- getGroupInfo db user groupId - lastItem <- getLastItem - stats <- maybe getChatStats pure stats_ + lastItem <- case lastItemId_ of + Just lastItemId -> (: []) <$> getGroupChatItem db user groupId lastItemId + Nothing -> pure [] pure $ AChat SCTGroup (Chat (GroupChat groupInfo) lastItem stats) - where - getLastItem :: ExceptT StoreError IO [CChatItem 'CTGroup] - getLastItem = - liftIO getLastItemId >>= \case - Nothing -> pure [] - Just lastItemId -> (: []) <$> getGroupChatItem db user groupId lastItemId - getLastItemId :: IO (Maybe ChatItemId) - getLastItemId = - maybeFirstRow fromOnly $ - DB.query - db - [sql| - SELECT chat_item_id FROM ( - SELECT group_id, chat_item_id, MAX(item_ts) - FROM chat_items - WHERE group_id = ? - GROUP BY group_id - ) - |] - (Only groupId) - getChatStats :: ExceptT StoreError IO ChatStats - getChatStats = do - r_ <- liftIO getUnreadStats - let (unreadCount, minUnreadItemId) = maybe (0, 0) (\(_, unreadCnt, minId) -> (unreadCnt, minId)) r_ - -- unread_chat could be read into group to not search twice - unreadChat <- - ExceptT . firstRow fromOnly (SEInternalError $ "unread_chat not found for group " <> show groupId) $ - DB.query db "SELECT unread_chat FROM groups WHERE group_id = ?" (Only groupId) - pure ChatStats {unreadCount, minUnreadItemId, unreadChat} - getUnreadStats :: IO (Maybe (GroupId, Int, ChatItemId)) - getUnreadStats = - maybeFirstRow id $ - DB.query - db - [sql| - SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread - FROM chat_items - WHERE group_id = ? AND item_status = ? - GROUP BY group_id - |] - (groupId, CISRcvNew) getContactRequestChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of