diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca1bc79510..26979df834 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -234,16 +234,14 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: docker exec -t builder sh -eu {0} run: | - scripts/desktop/build-lib-linux.sh - cd apps/multiplatform - ./gradlew packageDeb + scripts/desktop/make-deb-linux.sh - name: Prepare Desktop id: linux_desktop_build if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash run: | - path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_*_amd64.deb ) + path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_amd64.deb ) echo "package_path=$path" >> $GITHUB_OUTPUT echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT diff --git a/Dockerfile.build b/Dockerfile.build index 76bb1127f2..9b7c38fa87 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -16,6 +16,7 @@ RUN apt-get update && \ apt-get install -y curl \ libpq-dev \ git \ + strip-nondeterminism \ sqlite3 \ libsqlite3-dev \ build-essential \ diff --git a/README.md b/README.md index 40d552b84d..554c6068d9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design! -[](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) +[](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html)     [](https://www.privacyguides.org/en/real-time-communication/#simplex-chat)     [](https://www.whonix.org/wiki/Chat#Recommendation)     [](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) ## Welcome to SimpleX Chat! @@ -110,6 +110,15 @@ After you connect, you can [verify connection security code](./blog/20230103-sim Read about the app features and settings in the new [User guide](./docs/guide/README.md). +## Contribute + +We would love to have you join the development! You can help us with: + +- [share the color theme](./docs/THEMES.md) you use in Android app! +- writing a tutorial or recipes about hosting servers, chat bot automations, etc. +- contributing to SimpleX Chat knowledge-base. +- developing features - please connect to us via chat so we can help you get started. + ## Help translating SimpleX Chat Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages. @@ -141,15 +150,6 @@ Join our translators to help SimpleX grow! Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us! -## Contribute - -We would love to have you join the development! You can help us with: - -- [share the color theme](./docs/THEMES.md) you use in Android app! -- writing a tutorial or recipes about hosting servers, chat bot automations, etc. -- contributing to SimpleX Chat knowledge-base. -- developing features - please connect to us via chat so we can help you get started. - ## Please support us with your donations Huge thank you to everybody who donated to SimpleX Chat! diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 2ad8d546f2..180af685e5 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -45,21 +45,10 @@ struct ContentView: View { @State private var showChooseLAMode = false @State private var showSetPasscode = false @State private var waitingForOrPassedAuth = true - @State private var chatListActionSheet: ChatListActionSheet? = nil @State private var chatListUserPickerSheet: UserPickerSheet? = nil private let callTopPadding: CGFloat = 40 - private enum ChatListActionSheet: Identifiable { - case planAndConnectSheet(sheet: PlanAndConnectActionSheet) - - var id: String { - switch self { - case let .planAndConnectSheet(sheet): return sheet.id - } - } - } - private var accessAuthenticated: Bool { chatModel.contentViewAccessAuthenticated || contentAccessAuthenticationExtended } @@ -181,11 +170,6 @@ struct ContentView: View { if case .onboardingComplete = step, chatModel.currentUser != nil { mainView() - .actionSheet(item: $chatListActionSheet) { sheet in - switch sheet { - case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false) - } - } } else { OnboardingView(onboarding: step) } @@ -453,10 +437,7 @@ struct ContentView: View { let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") planAndConnect( link, - showAlert: showPlanAndConnectAlert, - showActionSheet: { chatListActionSheet = .planAndConnectSheet(sheet: $0) }, - dismiss: false, - incognito: nil + dismiss: false ) } else { AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid"))) @@ -479,10 +460,6 @@ struct ContentView: View { } } } - - private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) { - AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false)) - } } final class AlertManager: ObservableObject { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index fa7fc7cae4..c62c25c071 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -26,7 +26,6 @@ struct GroupMemberInfoView: View { @State private var knownContactConnectionStats: ConnectionStats? = nil @State private var newRole: GroupMemberRole = .member @State private var alert: GroupMemberInfoViewAlert? - @State private var sheet: PlanAndConnectActionSheet? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var justOpened = true @State private var progressIndicator = false @@ -41,7 +40,6 @@ struct GroupMemberInfoView: View { case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert - case planAndConnectAlert(alert: PlanAndConnectAlert) case queueInfo(info: String) case someAlert(alert: SomeAlert) case error(title: LocalizedStringKey, error: LocalizedStringKey?) @@ -57,7 +55,6 @@ struct GroupMemberInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" - case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" case let .queueInfo(info): return "queueInfo \(info)" case let .someAlert(alert): return "someAlert \(alert.id)" case let .error(title, _): return "error \(title)" @@ -270,13 +267,11 @@ struct GroupMemberInfoView: View { case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) - case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) case let .queueInfo(info): return queueInfoAlert(info) case let .someAlert(a): return a.alert case let .error(title, error): return mkAlert(title: title, message: error) } } - .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) } if progressIndicator { ProgressView().scaleEffect(2) @@ -350,10 +345,7 @@ struct GroupMemberInfoView: View { Button { planAndConnect( contactLink, - showAlert: { alert = .planAndConnectAlert(alert: $0) }, - showActionSheet: { sheet = $0 }, - dismiss: true, - incognito: nil + dismiss: true ) } label: { Label("Connect", systemImage: "link") diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 377764ac83..58e0882e38 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -579,8 +579,6 @@ struct ChatListSearchBar: View { @Binding var searchChatFilteredBySimplexLink: String? @Binding var parentSheet: SomeSheet? @State private var ignoreSearchTextChange = false - @State private var alert: PlanAndConnectAlert? - @State private var sheet: PlanAndConnectActionSheet? var body: some View { VStack(spacing: 12) { @@ -645,12 +643,6 @@ struct ChatListSearchBar: View { .onChange(of: chatTagsModel.activeFilter) { _ in searchText = "" } - .alert(item: $alert) { a in - planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) - } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" }) - } } private func toggleFilterButton() -> some View { @@ -676,10 +668,11 @@ struct ChatListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, dismiss: false, - incognito: nil, + cleanup: { + searchText = "" + searchFocussed = false + }, filterKnownContact: { searchChatFilteredBySimplexLink = $0.id }, filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id } ) diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index b8de0e4ceb..753d92b6d9 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -65,6 +65,27 @@ func showAlert( } } +func showSheet( + _ title: String?, + message: String? = nil, + actions: () -> [UIAlertAction] = { [okAlertAction] }, + sourceView: UIView? = nil // For iPad support +) { + if let topController = getTopViewController() { + let sheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + for action in actions() { sheet.addAction(action) } + + // Required for iPad: Configure popover presentation + if let popover = sheet.popoverPresentationController { + popover.sourceView = sourceView ?? topController.view + popover.sourceRect = sourceView?.bounds ?? CGRect(x: topController.view.bounds.midX, y: topController.view.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + } + + topController.present(sheet, animated: true) + } +} + let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default) let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 98f43f49b2..7a49c30b38 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -346,8 +346,6 @@ struct ContactsListSearchBar: View { @Binding var searchShowingSimplexLink: Bool @Binding var searchChatFilteredBySimplexLink: String? @State private var ignoreSearchTextChange = false - @State private var alert: PlanAndConnectAlert? - @State private var sheet: PlanAndConnectActionSheet? @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false var body: some View { @@ -416,12 +414,6 @@ struct ContactsListSearchBar: View { } } } - .alert(item: $alert) { a in - planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) - } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" }) - } } private func toggleFilterButton() -> some View { @@ -442,10 +434,11 @@ struct ContactsListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, dismiss: true, - incognito: nil, + cleanup: { + searchText = "" + searchFocussed = false + }, filterKnownContact: { searchChatFilteredBySimplexLink = $0.id } ) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 110eda7882..45c4f42a19 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -29,11 +29,9 @@ struct SomeSheet: Identifiable { } private enum NewChatViewAlert: Identifiable { - case planAndConnectAlert(alert: PlanAndConnectAlert) case newChatSomeAlert(alert: SomeAlert) var id: String { switch self { - case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" case let .newChatSomeAlert(alert): return "newChatSomeAlert \(alert.id)" } } @@ -165,8 +163,6 @@ struct NewChatView: View { } .alert(item: $alert) { a in switch(a) { - case let .planAndConnectAlert(alert): - return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" }) case let .newChatSomeAlert(a): return a.alert } @@ -593,7 +589,6 @@ private struct ConnectView: View { @Binding var showQRCodeScanner: Bool @Binding var pastedLink: String @Binding var alert: NewChatViewAlert? - @State private var sheet: PlanAndConnectActionSheet? @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings var body: some View { @@ -605,9 +600,6 @@ private struct ConnectView: View { ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode) } } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" }) - } } @ViewBuilder private func pasteLinkView() -> some View { @@ -662,10 +654,7 @@ private struct ConnectView: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = .planAndConnectAlert(alert: $0) }, - showActionSheet: { sheet = $0 }, - dismiss: true, - incognito: nil + dismiss: true ) } } @@ -839,187 +828,184 @@ func sharedProfileInfo(_ incognito: Bool) -> Text { ) } -enum PlanAndConnectAlert: Identifiable { - case ownInvitationLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case invitationLinkConnecting(connectionLink: CreatedConnLink) - case ownContactAddressConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case contactAddressConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnecting(connectionLink: CreatedConnLink, groupInfo: GroupInfo?) - case error(shortOrFullLink: String, alert: Alert) - - var id: String { - switch self { - case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink.connFullLink)" - case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink.connFullLink)" - case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink.connFullLink)" - case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink.connFullLink)" - case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink.connFullLink)" - case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink.connFullLink)" - case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink.connFullLink)" - case let .error(shortOrFullLink, alert): return "error \(shortOrFullLink)" - } - } +private func showInvitationLinkConnectingAlert(cleanup: (() -> Void)?) { + showAlert( + NSLocalizedString("Already connecting!", comment: "new chat sheet title"), + message: NSLocalizedString("You are already connecting via this one-time link!", comment: "new chat sheet message"), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} + ) } -func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (() -> Void)? = nil) -> Alert { - switch alert { - case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own one-time link!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case .invitationLinkConnecting: - return Alert( - title: Text("Already connecting!"), - message: Text("You are already connecting via this one-time link!"), - dismissButton: .default(Text("OK")) { cleanup?() } - ) - case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own SimpleX address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat connection request?"), - message: Text("You have already requested connection via this address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Join group?"), - message: Text("You will connect to all group members."), - primaryButton: .default( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat join request?"), - message: Text("You are already joining the group via this link!"), - primaryButton: .destructive( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConnecting(_, groupInfo): - if let groupInfo = groupInfo { - return groupInfo.businessChat == nil - ? Alert( - title: Text("Group already exists!"), - message: Text("You are already joining the group \(groupInfo.displayName)."), - dismissButton: .default(Text("OK")) { cleanup?() } - ) - : Alert( - title: Text("Chat already exists!"), - message: Text("You are already connecting to \(groupInfo.displayName)."), - dismissButton: .default(Text("OK")) { cleanup?() } +private func showGroupLinkConnectingAlert(groupInfo: GroupInfo?, cleanup: (() -> Void)?) { + if let groupInfo = groupInfo { + if groupInfo.businessChat == nil { + showAlert( + NSLocalizedString("Group already exists!", comment: "new chat sheet title"), + message: + String.localizedStringWithFormat( + NSLocalizedString("You are already joining the group %@.", comment: "new chat sheet message"), + groupInfo.displayName + ), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} ) } else { - return Alert( - title: Text("Already joining the group!"), - message: Text("You are already joining the group via this link."), - dismissButton: .default(Text("OK")) { cleanup?() } + showAlert( + NSLocalizedString("Chat already exists!", comment: "new chat sheet title"), + message: + String.localizedStringWithFormat( + NSLocalizedString("You are already connecting to %@.", comment: "new chat sheet message"), + groupInfo.displayName + ), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} ) } - case let .error(_, alert): return alert + } else { + showAlert( + NSLocalizedString("Already joining the group!", comment: "new chat sheet title"), + message: NSLocalizedString("You are already joining the group via this link.", comment: "new chat sheet message"), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} + ) } } -enum PlanAndConnectActionSheet: Identifiable { - case askCurrentOrIncognitoProfile(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileDestructive(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) - case ownGroupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) - - var id: String { - switch self { - case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink.connFullLink)" - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink.connFullLink)" - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)" - case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink.connFullLink)" +private func okCleanupAlertAction(cleanup: (() -> Void)?) -> UIAlertAction { + UIAlertAction( + title: NSLocalizedString("Ok", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() } - } + ) } -func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool, cleanup: (() -> Void)? = nil) -> ActionSheet { - switch sheet { - case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): - return ActionSheet( - title: Text("Connect with \(contact.chatViewName)"), - buttons: [ - .default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo): - if let incognito = incognito { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] +private func showAskCurrentOrIncognitoProfileSheet( + title: String, + actionStyle: UIAlertAction.Style = .default, + connectionLink: CreatedConnLink, + connectionPlan: ConnectionPlan?, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + title, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } ) - } else { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] + ]} + ) +} + +private func showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet( + contact: Contact, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + String.localizedStringWithFormat( + NSLocalizedString("Connect with %@", comment: "new chat action"), + contact.chatViewName + ), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: .default, + handler: { _ in + connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: .default, + handler: { _ in + connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } ) - } - } + ]} + ) +} + +private func showOwnGroupLinkConfirmConnectSheet( + groupInfo: GroupInfo, + connectionLink: CreatedConnLink, + connectionPlan: ConnectionPlan?, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + String.localizedStringWithFormat( + NSLocalizedString("Join your group?\nThis is your link for group %@!", comment: "new chat action"), + groupInfo.displayName + ), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Open group", comment: "new chat action"), + style: .default, + handler: { _ in + openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) + } + ), + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: .destructive, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: .destructive, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } + ) + ]} + ) } func planAndConnect( _ shortOrFullLink: String, - showAlert: @escaping (PlanAndConnectAlert) -> Void, - showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, dismiss: Bool, - incognito: Bool?, cleanup: (() -> Void)? = nil, filterKnownContact: ((Contact) -> Void)? = nil, filterKnownGroup: ((GroupInfo) -> Void)? = nil @@ -1031,25 +1017,30 @@ func planAndConnect( case let .invitationLink(ilp): switch ilp { case .ok: - logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - await MainActor.run { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) - } + logger.debug("planAndConnect, .invitationLink, .ok") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .ownLink: - logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .invitationLink, .ownLink") await MainActor.run { - if let incognito = incognito { - showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect to yourself?\nThis is your own one-time link!", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .connecting(contact_): - logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .invitationLink, .connecting") await MainActor.run { if let contact = contact_ { if let f = filterKnownContact { @@ -1058,11 +1049,11 @@ func planAndConnect( openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } } } else { - showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) + showInvitationLinkConnectingAlert(cleanup: cleanup) } } case let .known(contact): - logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .invitationLink, .known") await MainActor.run { if let f = filterKnownContact { f(contact) @@ -1074,34 +1065,42 @@ func planAndConnect( case let .contactAddress(cap): switch cap { case .ok: - logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - await MainActor.run { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) - } + logger.debug("planAndConnect, .contactAddress, .ok") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .ownLink: - logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .ownLink") await MainActor.run { - if let incognito = incognito { - showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect to yourself?\nThis is your own SimpleX address!", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .connectingConfirmReconnect: - logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect") await MainActor.run { - if let incognito = incognito { - showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("You have already requested connection!\nRepeat connection request?", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .connectingProhibit(contact): - logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .connectingProhibit") await MainActor.run { if let f = filterKnownContact { f(contact) @@ -1110,7 +1109,7 @@ func planAndConnect( } } case let .known(contact): - logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .known") await MainActor.run { if let f = filterKnownContact { f(contact) @@ -1119,49 +1118,60 @@ func planAndConnect( } } case let .contactViaAddress(contact): - logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - await MainActor.run { - showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) - } + logger.debug("planAndConnect, .contactAddress, .contactViaAddress") + await MainActor.run { + showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet( + contact: contact, + dismiss: dismiss, + cleanup: cleanup + ) } } case let .groupLink(glp): switch glp { case .ok: await MainActor.run { - if let incognito = incognito { - showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Join group", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .ownLink(groupInfo): - logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .ownLink") await MainActor.run { if let f = filterKnownGroup { f(groupInfo) } - showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) + showOwnGroupLinkConfirmConnectSheet( + groupInfo: groupInfo, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .connectingConfirmReconnect: - logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect") await MainActor.run { - if let incognito = incognito { - showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("You are already joining the group!\nRepeat join request?", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .connectingProhibit(groupInfo_): - logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .connectingProhibit") await MainActor.run { - showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) + showGroupLinkConnectingAlert(groupInfo: groupInfo_, cleanup: cleanup) } case let .known(groupInfo): - logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .known") await MainActor.run { if let f = filterKnownGroup { f(groupInfo) @@ -1172,15 +1182,20 @@ func planAndConnect( } case let .error(chatError): logger.debug("planAndConnect, .error \(chatErrorString(chatError))") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via link", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: nil, + dismiss: dismiss, + cleanup: cleanup + ) } } else if let alert { await MainActor.run { - showAlert(.error(shortOrFullLink: shortOrFullLink, alert: alert)) + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert(alert) + cleanup?() + } } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 20d921f836..554cef61fb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -184,8 +184,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -553,8 +553,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -712,8 +712,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -798,8 +798,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-BLKUepIn6JF2NJxeTwxEay.a */, ); path = Libraries; sourceTree = ""; diff --git a/cabal.project b/cabal.project index 56cafa6111..212f0ef2c7 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 4c33d8ac43874660c342c00cb078bd3814d5d9eb + tag: 1e82104224e2e63c18a9d50d8e0b253f4842512c source-repository-package type: git diff --git a/images/privacy-guides.jpg b/images/privacy-guides.jpg index f15a8862f7..5876d10c02 100644 Binary files a/images/privacy-guides.jpg and b/images/privacy-guides.jpg differ diff --git a/images/whonix-logo.jpg b/images/whonix-logo.jpg new file mode 100644 index 0000000000..c6fc4729b2 Binary files /dev/null and b/images/whonix-logo.jpg differ diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 6cc7aac011..ffd5e13337 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -37,13 +37,27 @@ cp *imple*.desktop usr/share/applications/ cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*.appdata.xml usr/share/metainfo if [ ! -f ../appimagetool-x86_64.AppImage ]; then - wget --secure-protocol=TLSv1_3 https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage + wget --secure-protocol=TLSv1_3 https://github.com/simplex-chat/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage chmod +x ../appimagetool-x86_64.AppImage fi if [ ! -f ../runtime-x86_64 ]; then - wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64 -O ../runtime-x86_64 + wget --secure-protocol=TLSv1_3 https://github.com/simplex-chat/type2-runtime/releases/download/continuous/runtime-x86_64 -O ../runtime-x86_64 chmod +x ../runtime-x86_64 fi -../appimagetool-x86_64.AppImage --runtime-file ../runtime-x86_64 . +# Determenistic build + +export SOURCE_DATE_EPOCH=1704067200 + +# Delete redundant jar file and modify cfg +rm -f ./usr/lib/app/*skiko-awt-runtime-linux* +sed -i -e '/skiko-awt-runtime-linux/d' ./usr/lib/app/simplex.cfg + +# Set all files to fixed time +find . -exec touch -d "@$SOURCE_DATE_EPOCH" {} + + +../appimagetool-x86_64.AppImage --verbose --no-appstream --runtime-file ../runtime-x86_64 . mv *imple*.AppImage ../../ + +# Just a safeguard +strip-nondeterminism ../../*imple*.AppImage diff --git a/scripts/desktop/make-deb-linux.sh b/scripts/desktop/make-deb-linux.sh new file mode 100755 index 0000000000..6f9de8a1e6 --- /dev/null +++ b/scripts/desktop/make-deb-linux.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh + +scripts/desktop/build-lib-linux.sh +cd apps/multiplatform +./gradlew packageDeb + +# Workaround for skiko library +# +# Compose Multiplatform depends on skiko library, that +# handles all of the window managment and graphics drawing. +# +# This skiko library comes with two jar's: +# - platform-agnostic "skiko-awt" +# - and platform-specific "skiko-awt-runtime" +# +# In case of Linux, second jar is called "skiko-awt-runtime-linux-x64". +# Essentially, this jar has the Linux .so library called "libskiko-linux-x64.so" +# that is being unpacked to runtime libs. +# +# Since the jar is nothing more than a zip archive, extracting library +# from "skiko-awt-runtime-linux-x64" modifies it's timestamps +# with current time, which in changes it's hash, which in turn +# makes the whole build unreproducible. +# +# It seems to be there is no way to handle this extraction in our code and +# https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives +# unfortunately doesn't solve the issue. +# +# Instead, just modify the deb, removing the redundant skiko library. +# +# Also, it seems this is related to: +# https://youtrack.jetbrains.com/issue/CMP-1971/createDistributable-produces-duplicated-skiko-awt.jar-and-skiko-awt-runtime-windows-x64.jar + +export SOURCE_DATE_EPOCH=1704067200 + +dpkg-deb -R ./release/main/deb/simplex*.deb ./extracted + +rm -f ./extracted/opt/*imple*/lib/app/*skiko-awt-runtime-linux* +sed -i -e '/skiko-awt-runtime-linux/d' ./extracted/opt/*imple*/lib/app/simplex.cfg +find ./extracted/ -exec touch -d "@$SOURCE_DATE_EPOCH" {} + + +dpkg-deb --build --root-owner-group --uniform-compression ./extracted ./release/main/deb/simplex_amd64.deb + +strip-nondeterminism ./release/main/deb/simplex_amd64.deb diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index bac315a1f4..cdaec76116 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."4c33d8ac43874660c342c00cb078bd3814d5d9eb" = "0223qr6vz3vigcx9279kmfxjwb7vwfsqgd7qz271wz86sfxg9hip"; + "https://github.com/simplex-chat/simplexmq.git"."1e82104224e2e63c18a9d50d8e0b253f4842512c" = "0qi8zxj94zch07h500980fyqqh5vjhvyy8i2swda2w5ky5jxljhy"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index d35e76f344..119cac8be6 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.0.2 +version: 6.4.0.3 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 016a7f3dd2..92fc6ed19f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -737,7 +737,7 @@ data ChatResponse | CRArchiveImported {archiveErrors :: [ArchiveError]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} #endif - | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} + | CRDebugLocks {chatLockName :: Maybe Text, chatEntityLocks :: Map Text Text, agentLocks :: AgentLocks} | CRAgentSubsTotal {user :: User, subsTotal :: SMPServerSubs, hasSession :: Bool} | CRAgentServersSummary {user :: User, serversSummary :: PresentedServersSummary} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 5d1709a468..78c2d1cc4f 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1667,7 +1667,8 @@ processChatCommand' vr = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode let userData = shortLinkUserData short - (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation userData Nothing IKPQOn subMode + -- TODO [certs rcv] + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' ConnNew incognitoProfile subMode initialChatVersion PQSupportOn @@ -1717,7 +1718,8 @@ processChatCommand' vr = \case recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do subMode <- chatReadVar subscriptionMode let userData = shortLinkUserData $ isJust $ connShortLink =<< connLinkInv - (agConnId, ccLink) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode + -- TODO [certs rcv] + (agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId @@ -1790,7 +1792,8 @@ processChatCommand' vr = \case APICreateMyAddress userId short -> withUserId userId $ \user -> procCmd $ do subMode <- chatReadVar subscriptionMode let userData = shortLinkUserData short - (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode + -- TODO [certs rcv] + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode pure $ CRUserContactLinkCreated user ccLink' @@ -2007,7 +2010,8 @@ processChatCommand' vr = \case Nothing -> do gVar <- asks random subMode <- chatReadVar subscriptionMode - (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + -- TODO [certs rcv] + (agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -2396,7 +2400,8 @@ processChatCommand' vr = \case subMode <- chatReadVar subscriptionMode let crClientData = encodeJSON $ CRDataGroup groupLinkId userData = shortLinkUserData short - (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode + -- TODO [certs rcv] + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo ccLink' mRole @@ -2437,7 +2442,8 @@ processChatCommand' vr = \case when (isJust $ memberContactId m) $ throwCmdError "member contact already exists" subMode <- chatReadVar subscriptionMode -- TODO PQ should negotitate contact connection with PQSupportOn? - (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + -- TODO [certs rcv] + (connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode -- TODO not sure it is correct to set connections status here? @@ -2689,12 +2695,12 @@ processChatCommand' vr = \case where getLocks ls = atomically $ M.mapKeys enityLockString . M.mapMaybe id <$> (mapM tryReadTMVar =<< readTVar ls) enityLockString cle = case cle of - CLInvitation bs -> "Invitation " <> B.unpack bs - CLConnection connId -> "Connection " <> show connId - CLContact ctId -> "Contact " <> show ctId - CLGroup gId -> "Group " <> show gId - CLUserContact ucId -> "UserContact " <> show ucId - CLFile fId -> "File " <> show fId + CLInvitation bs -> "Invitation " <> safeDecodeUtf8 bs + CLConnection connId -> "Connection " <> tshow connId + CLContact ctId -> "Contact " <> tshow ctId + CLGroup gId -> "Group " <> tshow gId + CLUserContact ucId -> "UserContact " <> tshow ucId + CLFile fId -> "File " <> tshow fId DebugEvent event -> toView event >> ok_ GetAgentSubsTotal userId -> withUserId userId $ \user -> do users <- withStore' $ \db -> getUsers db @@ -3719,8 +3725,9 @@ agentSubscriber = do where run action = action `catchChatError'` (eToView') -type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ())) +type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType (Maybe ClientServiceId))) +-- TODO [certs rcv] subscribeUserConnections :: VersionRangeChat -> Bool -> AgentBatchSubscribe -> User -> CM () subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do -- get user connections @@ -3814,7 +3821,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do pcs <- withStore_ getPendingContactConnections let connIds = map aConnId' pcs pure (connIds, M.fromList $ zip connIds pcs) - contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> CM () + contactSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId Contact -> Bool -> CM () contactSubsToView rs cts ce = do chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses) ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI @@ -3840,9 +3847,9 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted" e -> show e -- TODO possibly below could be replaced with less noisy events for API - contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () + contactLinkSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId UserContact -> CM () contactLinkSubsToView rs = toView . CEvtUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs - groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [ShortGroup] -> Map ConnId ShortGroupMember -> Bool -> CM () + groupSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> [ShortGroup] -> Map ConnId ShortGroupMember -> Bool -> CM () groupSubsToView rs gs ms ce = do mapM_ groupSub $ sortOn (\(ShortGroup ShortGroupInfo {groupName = g} _) -> g) gs @@ -3864,7 +3871,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do | membershipStatus == GSMemInvited = TEGroupInvitation user g | null members = TEGroupEmpty user g | otherwise = TEGroupSubscribed user g - sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM () + sndFileSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId SndFileTransfer -> CM () sndFileSubsToView rs sfts = do let sftRs = resultsFor rs sfts forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do @@ -3873,20 +3880,20 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do threadDelay 1000000 when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $ sendFileChunk user ft - rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM () + rcvFileSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId RcvFileTransfer -> CM () rcvFileSubsToView rs = mapM_ (toViewTE . uncurry (TERcvFileSubError user)) . filterErrors . resultsFor rs - pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM () + pendingConnSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId PendingContactConnection -> CM () pendingConnSubsToView rs = toViewTE . TEPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a] withStore_ a = withStore' (`a` user) `catchChatError` \e -> eToView e $> [] filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)] filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_) - resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)] + resultsFor :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId a -> [(a, Maybe ChatError)] resultsFor rs = M.foldrWithKey' addResult [] where addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)] addResult connId = (:) . (,resultErr connId rs) - resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError + resultErr :: ConnId -> Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Maybe ChatError resultErr connId rs = case M.lookup connId rs of Just (Left e) -> Just $ ChatErrorAgent e Nothing Just _ -> Nothing diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index bdc5e4b920..0f33c18a35 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -111,37 +111,37 @@ maxRcvMentions = 5 maxSndMentions :: Int maxSndMentions = 3 -withChatLock :: String -> CM a -> CM a +withChatLock :: Text -> CM a -> CM a withChatLock name action = asks chatLock >>= \l -> withLock l name action -withEntityLock :: String -> ChatLockEntity -> CM a -> CM a +withEntityLock :: Text -> ChatLockEntity -> CM a -> CM a withEntityLock name entity action = do chatLock <- asks chatLock ls <- asks entityLocks atomically $ unlessM (isEmptyTMVar chatLock) retry withLockMap ls entity name action -withInvitationLock :: String -> ByteString -> CM a -> CM a +withInvitationLock :: Text -> ByteString -> CM a -> CM a withInvitationLock name = withEntityLock name . CLInvitation {-# INLINE withInvitationLock #-} -withConnectionLock :: String -> Int64 -> CM a -> CM a +withConnectionLock :: Text -> Int64 -> CM a -> CM a withConnectionLock name = withEntityLock name . CLConnection {-# INLINE withConnectionLock #-} -withContactLock :: String -> ContactId -> CM a -> CM a +withContactLock :: Text -> ContactId -> CM a -> CM a withContactLock name = withEntityLock name . CLContact {-# INLINE withContactLock #-} -withGroupLock :: String -> GroupId -> CM a -> CM a +withGroupLock :: Text -> GroupId -> CM a -> CM a withGroupLock name = withEntityLock name . CLGroup {-# INLINE withGroupLock #-} -withUserContactLock :: String -> Int64 -> CM a -> CM a +withUserContactLock :: Text -> Int64 -> CM a -> CM a withUserContactLock name = withEntityLock name . CLUserContact {-# INLINE withUserContactLock #-} -withFileLock :: String -> Int64 -> CM a -> CM a +withFileLock :: Text -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} @@ -889,7 +889,8 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = pure (ct, conn, ExistingIncognito <$> incognitoProfile) let profileToSend = profileToSendOnAccept user incognitoProfile False dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - (ct,conn,) <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) + -- TODO [certs rcv] + (ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> PQSupport -> CM Contact acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile pqSup = do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 80e287f414..42c4cbc4f8 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -385,7 +385,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = agentMsgConnStatus :: AEvent e -> Maybe ConnStatus agentMsgConnStatus = \case - JOINED True -> Just ConnSndReady + JOINED True _ -> Just ConnSndReady CONF {} -> Just ConnRequested INFO {} -> Just ConnSndReady CON _ -> Just ConnReady @@ -430,7 +430,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED _ -> + -- TODO [certs rcv] + JOINED _ _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () QCONT -> @@ -449,7 +450,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () Just ct@Contact {contactId} -> case agentMsg of - INV (ACR _ cReq) -> + -- TODO [certs rcv] + INV (ACR _ cReq) _serviceId -> -- [async agent commands] XGrpMemIntro continuation on receiving INV withCompletedCommand conn agentMsg $ \_ -> case cReq of @@ -638,7 +640,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED sqSecured -> + -- TODO [certs rcv] + JOINED sqSecured _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (directOrUsed ct && sqSecured) $ do @@ -676,7 +679,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of - INV (ACR _ cReq) -> + -- TODO [certs rcv] + INV (ACR _ cReq) _serviceId -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cReq of groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of @@ -981,7 +985,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED sqSecured -> + -- TODO [certs rcv] + JOINED sqSecured _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (sqSecured && connChatVersion >= batchSend2Version) sendGroupAutoReply @@ -1095,7 +1100,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED _ -> + -- TODO [certs rcv] + JOINED _ _serviceId-> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () ERR err -> do @@ -1107,7 +1113,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processRcvFileConn :: AEvent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM () processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} = case agentMsg of - INV (ACR _ cReq) -> + -- TODO [certs rcv] + INV (ACR _ cReq) _serviceId -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cReq of fileInvConnReq@(CRInvitationUri _ _) -> case cmdFunction of @@ -1144,7 +1151,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED _ -> + -- TODO [certs rcv] + JOINED _ _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () MERR _ err -> do diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 92b499d8ef..f0a91e6d45 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 4, 0, 2] +minRemoteCtrlVersion = AppVersion [6, 4, 0, 3] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 4, 0, 2] +minRemoteHostVersion = AppVersion [6, 4, 0, 3] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index a9bd03ed0e..1c40586bc1 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -177,7 +177,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM #if defined(dbPostgres) -import Database.PostgreSQL.Simple (FromRow, Only (..), Query, ToRow, (:.) (..)) +import Database.PostgreSQL.Simple (FromRow, In (..), Only (..), Query, ToRow, (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) #else import Database.SQLite.Simple (FromRow, Only (..), Query, ToRow, (:.) (..)) @@ -2675,8 +2675,14 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb updateMemberCIsModerated :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO () updateMemberCIsModerated db User {userId} GroupInfo {groupId, membership} member byGroupMember md deletedTs = do itemIds <- updateCIs =<< getCurrentTime +#if defined(dbPostgres) + let inItemIds = Only $ In (map fromOnly itemIds) + DB.execute db "DELETE FROM messages WHERE message_id IN (SELECT message_id FROM chat_item_messages WHERE chat_item_id IN ?)" inItemIds + DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id IN ?" inItemIds +#else DB.executeMany db deleteChatItemMessagesQuery itemIds DB.executeMany db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" itemIds +#endif where memId = groupMemberId' member updateQuery = @@ -3199,7 +3205,7 @@ getGroupCIMentions db ciId = SELECT r.display_name, r.member_id, m.group_member_id, m.member_role, p.display_name, p.local_alias FROM chat_item_mentions r LEFT JOIN group_members m ON r.group_id = m.group_id AND r.member_id = m.member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) WHERE r.chat_item_id = ? |] (Only ciId) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index ea91bced4d..de30073f9b 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -3254,7 +3254,7 @@ Query: SELECT r.display_name, r.member_id, m.group_member_id, m.member_role, p.display_name, p.local_alias FROM chat_item_mentions r LEFT JOIN group_members m ON r.group_id = m.group_id AND r.member_id = m.member_id - LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) WHERE r.chat_item_id = ? Plan: diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index ce555c676c..9330e14aef 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -41,7 +41,7 @@ import Simplex.Chat.Types import Simplex.FileTransfer.Description (kb, mb) import Simplex.FileTransfer.Server (runXFTPServerBlocking) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration) -import Simplex.FileTransfer.Transport (supportedFileServerVRange) +import Simplex.FileTransfer.Transport (alpnSupportedXFTPhandshakes, supportedFileServerVRange) import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) @@ -55,10 +55,10 @@ import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) -import Simplex.Messaging.Server.Env.STM (AServerStoreCfg (..), ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) -import Simplex.Messaging.Server.MsgStore.Types (SQSType (..), SMSType (..)) +import Simplex.Messaging.Server.Env.STM (ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) +import Simplex.Messaging.Server.MsgStore.STM (STMMsgStore) import Simplex.Messaging.Transport -import Simplex.Messaging.Transport.Server (ServerCredentials (..), defaultTransportServerConfig) +import Simplex.Messaging.Transport.Server (ServerCredentials (..), mkTransportServerConfig) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive) @@ -491,7 +491,7 @@ testChatCfg5 cfg p1 p2 p3 p4 p5 test = testChatN cfg testOpts [p1, p2, p3, p4, p concurrentlyN_ :: [IO a] -> IO () concurrentlyN_ = mapConcurrently_ id -smpServerCfg :: ServerConfig +smpServerCfg :: ServerConfig STMMsgStore smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS, False)], @@ -501,7 +501,7 @@ smpServerCfg = maxJournalStateLines = 4, queueIdBytes = 24, msgIdBytes = 6, - serverStoreCfg = ASSCfg SQSMemory SMSMemory $ SSCMemory Nothing, + serverStoreCfg = SSCMemory Nothing, storeNtfsFile = Nothing, allowNewQueues = True, -- server password is disabled as otherwise v1 tests fail @@ -529,7 +529,7 @@ smpServerCfg = pendingENDInterval = 500000, ntfDeliveryInterval = 200000, smpServerVRange = supportedServerSMPRelayVRange, - transportConfig = defaultTransportServerConfig, + transportConfig = mkTransportServerConfig True (Just alpnSupportedSMPHandshakes) True, smpHandshakeTimeout = 1000000, controlPort = Nothing, smpAgentCfg = defaultSMPClientAgentConfig, @@ -539,13 +539,13 @@ smpServerCfg = startOptions = StartOptions {maintenance = False, compactLog = False, logLevel = LogError, skipWarnings = False, confirmMigrations = MCYesUp} } -persistentServerStoreCfg :: FilePath -> AServerStoreCfg -persistentServerStoreCfg tmp = ASSCfg SQSMemory SMSMemory $ SSCMemory $ Just StorePaths {storeLogFile = tmp <> "/smp-server-store.log", storeMsgsFile = Just $ tmp <> "/smp-server-messages.log"} +persistentServerStoreCfg :: FilePath -> ServerStoreCfg STMMsgStore +persistentServerStoreCfg tmp = SSCMemory $ Just StorePaths {storeLogFile = tmp <> "/smp-server-store.log", storeMsgsFile = Just $ tmp <> "/smp-server-messages.log"} withSmpServer :: IO () -> IO () withSmpServer = withSmpServer' smpServerCfg -withSmpServer' :: ServerConfig -> IO a -> IO a +withSmpServer' :: ServerConfig STMMsgStore -> IO a -> IO a withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing) xftpTestPort :: ServiceName @@ -582,7 +582,7 @@ xftpServerConfig = serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, controlPort = Nothing, - transportConfig = defaultTransportServerConfig, + transportConfig = mkTransportServerConfig True (Just alpnSupportedXFTPhandshakes) False, responseDelay = 0 } @@ -594,7 +594,7 @@ withXFTPServer' cfg = serverBracket ( \started -> do createDirectoryIfMissing False xftpServerFiles - runXFTPServerBlocking started cfg Nothing + runXFTPServerBlocking started cfg ) serverBracket :: (TMVar Bool -> IO ()) -> IO a -> IO a diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 1cf6d0d0d6..6b834acf01 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -84,7 +84,10 @@ chatGroupTests = do describe "batch send messages" $ do it "send multiple messages api" testSendMulti it "send multiple timed messages" testSendMultiTimed +#if !defined(dbPostgres) + -- TODO [postgres] this test hangs with PostgreSQL it "send multiple messages (many chat batches)" testSendMultiManyBatches +#endif xit'' "shared message body is reused" testSharedMessageBody xit'' "shared batch body is reused" testSharedBatchBody describe "async group connections" $ do @@ -1826,7 +1829,7 @@ testDeleteMemberWithMessages = do cath <## "alice updated group #team:" cath <## "updated group preferences:" - cath <## "Full deletion: on" + cath <## "Full deletion: on" ] threadDelay 750000 bob #> "#team hello" @@ -6792,7 +6795,7 @@ testForwardQuoteMention = bob <## " hello @alice @cath", do cath <# "#team alice!> -> forwarded" - cath <## " hello @alice @cath" + cath <## " hello @alice @cath" ] -- forward mentions alice `send` "@bob <- #team hello" diff --git a/website/src/_includes/hero.html b/website/src/_includes/hero.html index c0e2b3f30b..6ebfb883ef 100644 --- a/website/src/_includes/hero.html +++ b/website/src/_includes/hero.html @@ -31,6 +31,10 @@ Privacy Guides + + Whonix + + Heise Online @@ -43,7 +47,7 @@ Opt Out Podcast - +
@@ -66,7 +70,7 @@ - + {#

PLAY

diff --git a/website/src/img/whonix-dark.png b/website/src/img/whonix-dark.png new file mode 100644 index 0000000000..f361f4d1f6 Binary files /dev/null and b/website/src/img/whonix-dark.png differ diff --git a/website/src/img/whonix-light.png b/website/src/img/whonix-light.png new file mode 100644 index 0000000000..d8616b4a4b Binary files /dev/null and b/website/src/img/whonix-light.png differ