diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f28648d036..2b9cc7f770 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,6 +65,11 @@ jobs: asset_name: simplex-chat-ubuntu-22_04-x86-64 desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb - os: macos-latest + ghc: "9.6.3" + cache_path: ~/.cabal/store + asset_name: simplex-chat-macos-aarch64 + desktop_asset_name: simplex-desktop-macos-aarch64.dmg + - os: macos-13 ghc: "9.6.3" cache_path: ~/.cabal/store asset_name: simplex-chat-macos-x86-64 @@ -107,18 +112,36 @@ jobs: if: matrix.os == 'macos-latest' shell: bash run: | - echo "ignore-project: False" >> cabal.project.local - echo "package direct-sqlcipher" >> cabal.project.local - echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local - echo " flags: +openssl" >> cabal.project.local + echo "ignore-project: False" >> cabal.project.local + echo "package simplexmq" >> cabal.project.local + echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local + echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local + echo "" >> cabal.project.local + echo "package direct-sqlcipher" >> cabal.project.local + echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local + echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local + echo " flags: +openssl" >> cabal.project.local + + - name: Unix prepare cabal.project.local for Mac + if: matrix.os == 'macos-13' + shell: bash + run: | + echo "ignore-project: False" >> cabal.project.local + echo "package simplexmq" >> cabal.project.local + echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local + echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local + echo "" >> cabal.project.local + echo "package direct-sqlcipher" >> cabal.project.local + echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local + echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local + echo " flags: +openssl" >> cabal.project.local - name: Install AppImage dependencies if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04' run: sudo apt install -y desktop-file-utils - name: Install pkg-config for Mac - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-latest' || matrix.os == 'macos-13' run: brew install pkg-config - name: Unix prepare cabal.project.local for Ubuntu @@ -190,7 +213,7 @@ jobs: - name: Mac build desktop id: mac_desktop_build - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13') shell: bash env: APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }} @@ -241,7 +264,7 @@ jobs: ${{ steps.linux_appimage_build.outputs.appimage_hash }} - name: Mac upload desktop package to release - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13') uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} @@ -250,7 +273,7 @@ jobs: tag: ${{ github.ref }} - name: Mac update desktop package hash - if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'macos-latest' || matrix.os == 'macos-13') uses: softprops/action-gh-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 72a7cf2b94..b396c9a289 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -5,14 +5,20 @@ on: pull_request_target: types: [opened, closed, synchronize] +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + jobs: CLAssistant: runs-on: ubuntu-latest steps: - name: "CLA Assistant" - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request' + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' # Beta Release - uses: cla-assistant/github-action@v2.1.3-beta + uses: cla-assistant/github-action@v2.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the below token should have repo scope and must be manually added by you in the repository's secret @@ -33,4 +39,4 @@ jobs: #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) - #use-dco-flag: true - If you are using DCO instead of CLA \ No newline at end of file + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 039c136464..7fc66308f8 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -4,7 +4,6 @@ on: push: branches: - master - - stable paths: - website/** - images/** diff --git a/.gitignore b/.gitignore index d7106ec8fa..e3ea5d267b 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ website/translations.json website/src/img/images/ website/src/images/ website/src/js/lottie.min.js +website/src/privacy.md # Generated files website/package/generated* diff --git a/PRIVACY.md b/PRIVACY.md index 3204fa1e53..3b4ba2ba8a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,16 +1,20 @@ +--- +layout: layouts/privacy.html +--- + # SimpleX Chat Privacy Policy and Conditions of Use SimpleX Chat is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability. SimpleX Chat communication protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX Chat apps allow their users to send messages and files via relay server infrastructure. Relay server owners and providers do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not provide any user accounts. -Double ratchet algorithm has such important properties as [forward secrecy](./docs/GLOSSARY.md#forward-secrecy), sender [repudiation](./docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](./docs/GLOSSARY.md#post-compromise-security)). +Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)). If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). ## Privacy Policy -SimpleX Chat Ltd uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](./docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](./docs/GLOSSARY.md#man-in-the-middle-attack), unlike most other communication platforms, services and networks. +SimpleX Chat Ltd uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack), unlike most other communication platforms, services and networks. SimpleX Chat software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having any kind of addresses or other identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications. @@ -20,7 +24,7 @@ While SimpleX Chat Ltd is not a communication service provider, and provide publ We see users and data sovereignty, and device and provider portability as critically important properties for any communication system. -SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 – see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). ### Your information @@ -34,7 +38,7 @@ You can transfer the profile to another device by creating a backup of the app d #### Messages and Files -SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 8mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](./docs/GLOSSARY.md#key-exchange) happens out-of-band. +SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 8mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](/docs/GLOSSARY.md#key-exchange) happens out-of-band. Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline, messaging relay servers temporarily store end-to-end encrypted messages – you can configure which relay servers are used to receive the messages from the new contacts, and you can manually change them for the existing contacts too. @@ -82,7 +86,7 @@ Additional technical information can be stored on our servers, including randoml #### SimpleX Directory -[SimpleX Directory](./docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). +[SimpleX Directory](/docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). #### User Support @@ -103,6 +107,8 @@ The cases when SimpleX Chat Ltd may share the data temporarily stored on the ser At the time of updating this document, we have never provided or have been requested the access to the preset relay servers or any information from the servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process to limit any information shared with the third parties to the minimally required by law. +We will publish information we are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). + ### Updates We will update this Privacy Policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our software applications and preset relays infrastructure confirms your acceptance of our updated Privacy Policy. @@ -131,7 +137,7 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using our Applications, and any associated taxes. -**Legal and acceptable usage**. You agree to use our Applications only for legal and acceptable purposes. You will not use (or assist others in using) our Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal or impermissible communications, e.g. spam. While we cannot access content or identify messages or groups, in some cases the links to the illegal or impermissible communications available via our Applications can be shared publicly on social media or websites. We reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via our servers, whether they were reported by the users or discovered by our team. +**Legal usage**. You agree to use our Applications only for legal purposes. You will not use (or assist others in using) our Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While we cannot access content or identify messages or groups, in some cases the links to the illegal communications available via our Applications can be shared publicly on social media or websites. We reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via our servers, whether they were reported by the users or discovered by our team. **Damage to SimpleX Chat Ltd**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, our Infrastructure, or any other systems. For example, you must not 1) access our Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of our Infrastructure; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software. @@ -165,4 +171,4 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd. -Updated February 24, 2024 +Updated April 24, 2024 diff --git a/README.md b/README.md index 28b40d0c1b..697f5ca08e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) ![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social) -| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) | +| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) | SimpleX logo @@ -74,7 +74,7 @@ Messages not following these rules will be deleted, the right to send messages m You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) -There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform: +There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform: - chat bots and automations - integrations with other apps @@ -234,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) + [Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) [Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) @@ -412,13 +414,13 @@ We have never provided or have been requested access to our servers or any infor We do not log IP addresses of the users and we do not perform any traffic correlation on our servers. If transport level security is critical you must use Tor or some other similar network to access messaging servers. We will be improving the client applications to reduce the opportunities for traffic correlation. -Please read more in [Terms & privacy policy](./PRIVACY.md). +Please read more in [Privacy Policy](./PRIVACY.md). ## Security contact -To report a security vulnerability, please send us email to chat@simplex.chat. We will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues. +Please see our [Security Policy](./docs/SECURITY.md) on how to report security vulnerabilities to us. We will coordinate the fix and disclosure. -Please treat any findings of possible traffic correlation attacks allowing to correlate two different conversations to the same user, other than covered in [the threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model), as security vulnerabilities, and follow this disclosure process. +Please do NOT report security vulnerabilities via GitHub issues. ## License diff --git a/apps/ios/Shared/Model/AudioRecPlay.swift b/apps/ios/Shared/Model/AudioRecPlay.swift index a9d0d6c1d9..99851f4be8 100644 --- a/apps/ios/Shared/Model/AudioRecPlay.swift +++ b/apps/ios/Shared/Model/AudioRecPlay.swift @@ -179,7 +179,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { if playback { if AVAudioSession.sharedInstance().category != .playback { logger.log("AudioSession: playback") - try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: .duckOthers) + try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: [.duckOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) } } else { if AVAudioSession.sharedInstance().category != .soloAmbient { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 462699e407..a4651e1d42 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -103,6 +103,7 @@ final class ChatModel: ObservableObject { // tracks keyboard height via subscription in AppDelegate @Published var keyboardHeight: CGFloat = 0 @Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings + @Published var networkInfo = UserNetworkInfo(networkType: .other, online: true) var messageDelivery: Dictionary Void> = [:] @@ -778,6 +779,24 @@ final class Chat: ObservableObject, Identifiable { var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } } + func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { + if case let .group(groupInfo) = self.chatInfo { + let p = groupInfo.fullGroupPreferences + return switch feature { + case .timedMessages: p.timedMessages.on + case .directMessages: p.directMessages.on(for: groupInfo.membership) + case .fullDelete: p.fullDelete.on + case .reactions: p.reactions.on + case .voice: p.voice.on(for: groupInfo.membership) + case .files: p.files.on(for: groupInfo.membership) + case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) + case .history: p.history.on + } + } else { + return true + } + } + public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) } diff --git a/apps/ios/Shared/Model/NetworkObserver.swift b/apps/ios/Shared/Model/NetworkObserver.swift new file mode 100644 index 0000000000..84c35afa07 --- /dev/null +++ b/apps/ios/Shared/Model/NetworkObserver.swift @@ -0,0 +1,73 @@ +// +// NetworkObserver.swift +// SimpleX (iOS) +// +// Created by Avently on 05.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import Network +import SimpleXChat + +class NetworkObserver { + static let shared = NetworkObserver() + private let queue: DispatchQueue = DispatchQueue(label: "chat.simplex.app.NetworkObserver") + private var prevInfo: UserNetworkInfo? = nil + private var monitor: NWPathMonitor? + private let monitorLock: DispatchQueue = DispatchQueue(label: "chat.simplex.app.monitorLock") + + func restartMonitor() { + monitorLock.sync { + monitor?.cancel() + let mon = NWPathMonitor() + mon.pathUpdateHandler = { [weak self] path in + self?.networkPathChanged(path: path) + } + mon.start(queue: queue) + monitor = mon + } + } + + private func networkPathChanged(path: NWPath) { + let info = UserNetworkInfo( + networkType: networkTypeFromPath(path), + online: path.status == .satisfied + ) + if (prevInfo != info) { + prevInfo = info + setNetworkInfo(info) + } + } + + private func networkTypeFromPath(_ path: NWPath) -> UserNetworkType { + if path.usesInterfaceType(.wiredEthernet) { + .ethernet + } else if path.usesInterfaceType(.wifi) { + .wifi + } else if path.usesInterfaceType(.cellular) { + .cellular + } else if path.usesInterfaceType(.other) { + .other + } else { + .none + } + } + + private static var networkObserver: NetworkObserver? = nil + + private func setNetworkInfo(_ info: UserNetworkInfo) { + logger.debug("setNetworkInfo Network changed: \(String(describing: info))") + DispatchQueue.main.sync { + ChatModel.shared.networkInfo = info + } + if !hasChatCtrl() { return } + self.monitorLock.sync { + do { + try apiSetNetworkInfo(info) + } catch let err { + logger.error("setNetworkInfo error: \(responseError(err))") + } + } + } +} diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a099069f77..b4c9a48d5d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -272,18 +272,6 @@ func apiGetAppSettings(settings: AppSettings) throws -> AppSettings { throw r } -func apiSetPQEncryption(_ enable: Bool) throws { - let r = chatSendCmdSync(.apiSetPQEncryption(enable: enable)) - if case .cmdOk = r { return } - throw r -} - -func apiSetContactPQ(_ contactId: Int64, _ enable: Bool) async throws -> Contact { - let r = await chatSendCmd(.apiSetContactPQ(contactId: contactId, enable: enable)) - if case let .contactPQAllowed(_, contact, _) = r { return contact } - throw r -} - func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } @@ -353,11 +341,20 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws - throw r } +func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? { + let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId) + return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) +} + func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? { - let chatModel = ChatModel.shared let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl) + return await processSendMessageCmd(toChatType: type, cmd: cmd) +} + +private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> ChatItem? { + let chatModel = ChatModel.shared let r: ChatResponse - if type == .direct { + if toChatType == .direct { var cItem: ChatItem? = nil let endTask = beginBGTask({ if let cItem = cItem { @@ -397,7 +394,7 @@ func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) } private func sendMessageErrorAlert(_ r: ChatResponse) { - logger.error("apiSendMessage error: \(String(describing: r))") + logger.error("send message error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error sending message", message: "Error: \(String(describing: r))" @@ -534,6 +531,12 @@ func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws { throw r } +func apiSetNetworkInfo(_ networkInfo: UserNetworkInfo) throws { + let r = chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo)) + if case .cmdOk = r { return } + throw r +} + func reconnectAllServers() async throws { try await sendCommandOkResp(.reconnectAllServers) } @@ -1297,6 +1300,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni defer { m.ctrlInitInProgress = false } (m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations) if m.chatDbStatus != .ok { return } + NetworkObserver.shared.restartMonitor() // If we migrated successfully means previous re-encryption process on database level finished successfully too if encryptionStartedDefault.get() { encryptionStartedDefault.set(false) @@ -1304,7 +1308,6 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) - try apiSetPQEncryption(pqExperimentalEnabledDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() if m.currentUser == nil { @@ -1462,6 +1465,8 @@ class ChatReceiver { private var receiveMessages = true private var _lastMsgTime = Date.now + var messagesChannel: ((ChatResponse) -> Void)? = nil + static let shared = ChatReceiver() var lastMsgTime: Date { get { _lastMsgTime } } @@ -1479,6 +1484,9 @@ class ChatReceiver { if let msg = await chatRecvMsg() { self._lastMsgTime = .now await processReceivedMsg(msg) + if let messagesChannel { + messagesChannel(msg) + } } _ = try? await Task.sleep(nanoseconds: 7_500_000) } @@ -1791,7 +1799,6 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .sndFileCompleteXFTP(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) - Task { cleanupFile(aChatItem) } case let .sndFileError(user, aChatItem, _): if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 9f246f63f3..97415018bf 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -9,6 +9,7 @@ import SwiftUI import WebKit import SimpleXChat +import AVFoundation struct ActiveCallView: View { @EnvironmentObject var m: ChatModel @@ -21,6 +22,7 @@ struct ActiveCallView: View { @Binding var canConnectCall: Bool @State var prevColorScheme: ColorScheme = .dark @State var pipShown = false + @State var wasConnected = false var body: some View { ZStack(alignment: .topLeading) { @@ -69,6 +71,11 @@ struct ActiveCallView: View { Task { await m.callCommand.setClient(nil) } AppDelegate.keepScreenOn(false) client?.endCall() + CallSoundsPlayer.shared.stop() + try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) + if (wasConnected) { + CallSoundsPlayer.shared.vibrate(long: true) + } } .background(m.activeCallViewIsCollapsed ? .clear : .black) // Quite a big delay when opening/closing the view when a scheme changes (globally) this way. It's not needed when CallKit is used since status bar is green with white text on it @@ -103,6 +110,11 @@ struct ActiveCallView: View { call.callState = .invitationSent call.localCapabilities = capabilities } + if call.supportsVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() { + try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + } + CallSoundsPlayer.shared.startConnectingCallSound() + activeCallWaitDeliveryReceipt() } case let .offer(offer, iceCandidates, capabilities): Task { @@ -126,6 +138,7 @@ struct ActiveCallView: View { } await MainActor.run { call.callState = .negotiated + CallSoundsPlayer.shared.stop() } } case let .ice(iceCandidates): @@ -144,6 +157,10 @@ struct ActiveCallView: View { : CallController.shared.reportIncomingCall(call: call, connectedAt: nil) call.callState = .connected call.connectedAt = .now + if !wasConnected { + CallSoundsPlayer.shared.vibrate(long: false) + wasConnected = true + } } if state.connectionState == "closed" { closeCallView(client) @@ -161,6 +178,10 @@ struct ActiveCallView: View { call.callState = .connected call.connectionInfo = connectionInfo call.connectedAt = .now + if !wasConnected { + CallSoundsPlayer.shared.vibrate(long: false) + wasConnected = true + } case .ended: closeCallView(client) call.callState = .ended @@ -187,6 +208,22 @@ struct ActiveCallView: View { } } + private func activeCallWaitDeliveryReceipt() { + ChatReceiver.shared.messagesChannel = { msg in + guard let call = ChatModel.shared.activeCall, call.callState == .invitationSent else { + ChatReceiver.shared.messagesChannel = nil + return + } + if case let .chatItemStatusUpdated(_, msg) = msg, + msg.chatInfo.id == call.contact.id, + case .sndCall = msg.chatItem.content, + case .sndRcvd = msg.chatItem.meta.itemStatus { + CallSoundsPlayer.shared.startInCallSound() + ChatReceiver.shared.messagesChannel = nil + } + } + } + private func closeCallView(_ client: WebRTCClient) { if m.activeCall != nil { m.showCallView = false @@ -198,6 +235,7 @@ struct ActiveCallOverlay: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var call: Call var client: WebRTCClient + @ObservedObject private var deviceManager = CallAudioDeviceManager.shared var body: some View { VStack { @@ -213,7 +251,16 @@ struct ActiveCallOverlay: View { HStack { toggleAudioButton() Spacer() - Color.clear.frame(width: 40, height: 40) + if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) { + toggleSpeakerButton() + .frame(width: 40, height: 40) + } else if call.hasMedia { + AudioDevicePicker() + .scaleEffect(2) + .frame(maxWidth: 40, maxHeight: 40) + } else { + Color.clear.frame(width: 40, height: 40) + } Spacer() endCallButton() Spacer() @@ -239,9 +286,7 @@ struct ActiveCallOverlay: View { .foregroundColor(.white.opacity(0.8)) } VStack { - ProfileImage(imageStr: call.contact.profile.image) - .scaledToFit() - .frame(width: 192, height: 192) + ProfileImage(imageStr: call.contact.profile.image, size: 192) audioCallInfoView(call) } .foregroundColor(.white) @@ -256,14 +301,33 @@ struct ActiveCallOverlay: View { toggleAudioButton() .frame(maxWidth: .infinity, alignment: .leading) endCallButton() - toggleSpeakerButton() - .frame(maxWidth: .infinity, alignment: .trailing) + // Check if the only input is microphone. And in this case show toggle button, + // If there are more inputs, it probably means something like bluetooth headphones are available + // and in this case show iOS button for choosing different output. + // There is no way to get available outputs, only inputs + if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) { + toggleSpeakerButton() + .frame(maxWidth: .infinity, alignment: .trailing) + } else if call.hasMedia { + AudioDevicePicker() + .scaleEffect(2) + .frame(maxWidth: 50, maxHeight: 40) + .frame(maxWidth: .infinity, alignment: .trailing) + } else { + Color.clear.frame(width: 50, height: 40) + } } .padding(.bottom, 60) .padding(.horizontal, 48) } } .frame(maxWidth: .infinity) + .onAppear { + deviceManager.start() + } + .onDisappear { + deviceManager.stop() + } } private func audioCallInfoView(_ call: Call) -> some View { @@ -341,12 +405,17 @@ struct ActiveCallOverlay: View { private func toggleSpeakerButton() -> some View { controlButton(call, call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill") { Task { - client.setSpeakerEnabledAndConfigureSession(!call.speakerEnabled) + let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker + client.setSpeakerEnabledAndConfigureSession(!speakerEnabled) DispatchQueue.main.async { - call.speakerEnabled = !call.speakerEnabled + call.speakerEnabled = !speakerEnabled } } } + .onAppear { + deviceManager.call = call + //call.speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker + } } private func toggleVideoButton() -> some View { diff --git a/apps/ios/Shared/Views/Call/AudioDevicePicker.swift b/apps/ios/Shared/Views/Call/AudioDevicePicker.swift new file mode 100644 index 0000000000..3d846c7b68 --- /dev/null +++ b/apps/ios/Shared/Views/Call/AudioDevicePicker.swift @@ -0,0 +1,25 @@ +// +// MPVolumeView.swift +// SimpleX (iOS) +// +// Created by Stanislav on 24.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import UIKit +import AVKit + +struct AudioDevicePicker: UIViewRepresentable { + func makeUIView(context: Context) -> some UIView { + let v = AVRoutePickerView(frame: .zero) + v.activeTintColor = .white + v.tintColor = .white + return v + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } +} diff --git a/apps/ios/Shared/Views/Call/CallAudioDeviceManager.swift b/apps/ios/Shared/Views/Call/CallAudioDeviceManager.swift new file mode 100644 index 0000000000..d56849d16a --- /dev/null +++ b/apps/ios/Shared/Views/Call/CallAudioDeviceManager.swift @@ -0,0 +1,67 @@ +// +// CallAudioDeviceManager.swift +// SimpleX (iOS) +// +// Created by Avently on 23.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat +import AVKit +import WebRTC + +class CallAudioDeviceManager: ObservableObject { + static let shared = CallAudioDeviceManager() + let audioSession: AVAudioSession + let nc = NotificationCenter.default + + var call: Call? + var timer: Timer? = nil + + // Actually, only one output + @Published var outputs: [AVAudioSessionPortDescription] + @Published var currentDevice: AVAudioSessionPortDescription? = nil + // All devices that can record audio (the ones that can play audio are not included) + @Published var availableInputs: [AVAudioSessionPortDescription] = [] + + + init(_ audioSession: AVAudioSession? = nil) { + self.audioSession = audioSession ?? RTCAudioSession.sharedInstance().session + self.outputs = self.audioSession.currentRoute.outputs + self.availableInputs = self.audioSession.availableInputs ?? [] + } + + func reloadDevices() { + outputs = audioSession.currentRoute.outputs + currentDevice = audioSession.currentRoute.outputs.first + availableInputs = audioSession.availableInputs ?? [] + call?.speakerEnabled = currentDevice?.portType == .builtInSpeaker + + + // Workaround situation: + // have bluetooth device connected, choosing speaker, disconnecting bluetooth device. In this case iOS will not post notification, so do it manually + timer?.invalidate() + if availableInputs.contains(where: { $0.portType != .builtInReceiver && $0.portType != .builtInSpeaker }) { + timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { t in + self.reloadDevices() + } + } + } + + @objc func audioCallback(notification: Notification) { + reloadDevices() + + logger.debug("Changes in devices, current audio devices: \(String(describing: self.availableInputs.map({ $0.portType.rawValue }))), output: \(String(describing: self.currentDevice?.portType.rawValue))") + } + + func start() { + nc.addObserver(self, selector: #selector(audioCallback), name: AVAudioSession.routeChangeNotification, object: nil) + } + + func stop() { + nc.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil) + timer?.invalidate() + } +} diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index 6da8294ef8..64b565e8e6 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -103,7 +103,23 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession) RTCAudioSession.sharedInstance().isAudioEnabled = true do { - try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers) + let supportsVideo = ChatModel.shared.activeCall?.supportsVideo == true + if supportsVideo { + try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + } else { + try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + } + // Without any delay sound is not playing from speaker or external device in incoming call + Task { + for i in 0 ... 3 { + try? await Task.sleep(nanoseconds: UInt64(i) * 300_000000) + if let preferred = audioSession.preferredInputDevice() { + await MainActor.run { try? audioSession.setPreferredInput(preferred) } + } else if supportsVideo { + await MainActor.run { try? audioSession.overrideOutputAudioPort(.speaker) } + } + } + } logger.debug("audioSession category set") try audioSession.setActive(true) logger.debug("audioSession activated") diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index c2d5dabd48..4647995b28 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -30,8 +30,7 @@ struct IncomingCallView: View { VStack(alignment: .leading, spacing: 6) { HStack { if m.users.count > 1 { - ProfileImage(imageStr: invitation.user.image, color: .white) - .frame(width: 24, height: 24) + ProfileImage(imageStr: invitation.user.image, size: 24, color: .white) } Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green) Text(invitation.callTypeText) diff --git a/apps/ios/Shared/Views/Call/SoundPlayer.swift b/apps/ios/Shared/Views/Call/SoundPlayer.swift index 17c13ab403..c7803a0cb8 100644 --- a/apps/ios/Shared/Views/Call/SoundPlayer.swift +++ b/apps/ios/Shared/Views/Call/SoundPlayer.swift @@ -8,6 +8,7 @@ import Foundation import AVFoundation +import UIKit class SoundPlayer { static let shared = SoundPlayer() @@ -43,3 +44,63 @@ class SoundPlayer { audioPlayer = nil } } + +class CallSoundsPlayer { + static let shared = CallSoundsPlayer() + private var audioPlayer: AVAudioPlayer? + private var playerTask: Task = Task {} + + private func start(_ soundName: String, delayMs: Double) { + audioPlayer?.stop() + playerTask.cancel() + logger.debug("start \(soundName)") + guard let path = Bundle.main.path(forResource: soundName, ofType: "mp3", inDirectory: "sounds") else { + logger.debug("start: file not found") + return + } + do { + let player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) + if player.prepareToPlay() { + audioPlayer = player + } + } catch { + logger.debug("start: AVAudioPlayer error \(error.localizedDescription)") + } + + playerTask = Task { + while let player = audioPlayer { + player.play() + do { + try await Task.sleep(nanoseconds: UInt64((player.duration * 1_000_000_000) + delayMs * 1_000_000)) + } catch { + break + } + } + } + } + + func startConnectingCallSound() { + start("connecting_call", delayMs: 0) + } + + func startInCallSound() { + // Taken from https://github.com/TelegramOrg/Telegram-Android + // https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE + start("in_call", delayMs: 1000) + } + + func stop() { + playerTask.cancel() + audioPlayer?.stop() + audioPlayer = nil + } + + func vibrate(long: Bool) { + // iOS just don't want to vibrate more than once after a short period of time, and all 'styles' feel the same + if long { + AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) + } else { + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + } + } +} diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index 919b1e14e7..333dc082d5 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -431,17 +431,18 @@ struct RTCIceServer: Codable, Equatable { } // the servers are expected in this format: -// stun:stun.simplex.im:443?transport=tcp -// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp +// stuns:stun.simplex.im:443?transport=tcp +// turns:private2:Hxuq2QxUjnhj96Zq2r4HjqHRj@turn.simplex.im:443?transport=tcp func parseRTCIceServer(_ str: String) -> RTCIceServer? { var s = replaceScheme(str, "stun:") + s = replaceScheme(s, "stuns:") s = replaceScheme(s, "turn:") s = replaceScheme(s, "turns:") if let u: URL = URL(string: s), let scheme = u.scheme, let host = u.host, let port = u.port, - u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns") { + u.path == "" && (scheme == "stun" || scheme == "stuns" || scheme == "turn" || scheme == "turns") { let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "") return RTCIceServer( urls: ["\(scheme):\(host):\(port)\(query)"], diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift index 1806984d64..0b4917c103 100644 --- a/apps/ios/Shared/Views/Call/WebRTCClient.swift +++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift @@ -49,7 +49,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } private let rtcAudioSession = RTCAudioSession.sharedInstance() - private let audioQueue = DispatchQueue(label: "audio") + private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio") private var sendCallResponse: (WVAPIMessage) async -> Void var activeCall: Binding private var localRendererAspectRatio: Binding @@ -65,14 +65,14 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg self.localRendererAspectRatio = localRendererAspectRatio rtcAudioSession.useManualAudio = CallController.useCallKit() rtcAudioSession.isAudioEnabled = !CallController.useCallKit() - logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)}") + logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)") super.init() } let defaultIceServers: [WebRTC.RTCIceServer] = [ - WebRTC.RTCIceServer(urlStrings: ["stun:stun.simplex.im:443"]), - WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"), - WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"), + WebRTC.RTCIceServer(urlStrings: ["stuns:stun.simplex.im:443"]), + //WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"), + WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"), ] func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call { @@ -605,9 +605,23 @@ extension WebRTCClient { self.rtcAudioSession.unlockForConfiguration() } do { - try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) - try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) - try self.rtcAudioSession.overrideOutputAudioPort(enabled ? .speaker : .none) + let hasExternalAudioDevice = self.rtcAudioSession.session.hasExternalAudioDevice() + if enabled { + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.defaultToSpeaker, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + try self.rtcAudioSession.setMode(AVAudioSession.Mode.videoChat.rawValue) + if hasExternalAudioDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() { + try self.rtcAudioSession.setPreferredInput(preferred) + } else { + try self.rtcAudioSession.overrideOutputAudioPort(.speaker) + } + } else { + try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP]) + try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) + try self.rtcAudioSession.overrideOutputAudioPort(.none) + } + if hasExternalAudioDevice { + logger.debug("WebRTCClient: configuring session with external device available, skip configuring speaker") + } try self.rtcAudioSession.setActive(true) logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled) success") } catch let error { @@ -658,6 +672,17 @@ extension WebRTCClient { } } +extension AVAudioSession { + func hasExternalAudioDevice() -> Bool { + availableInputs?.allSatisfy({ $0.portType == .builtInMic }) != true + } + + func preferredInputDevice() -> AVAudioSessionPortDescription? { +// logger.debug("Preferred input device: \(String(describing: self.availableInputs?.filter({ $0.portType != .builtInMic })))") + return availableInputs?.filter({ $0.portType != .builtInMic }).last + } +} + struct CustomRTCSessionDescription: Codable { public var type: RTCSdpType? public var sdp: String? diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index d0f4b6e55a..140b609902 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -25,11 +25,11 @@ struct ChatInfoToolbar: View { } ChatInfoImage( chat: chat, + size: imageSize, color: colorScheme == .dark ? chatImageColorDark : chatImageColorLight ) - .frame(width: imageSize, height: imageSize) .padding(.trailing, 4) VStack { let t = Text(cInfo.displayName).font(.headline) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 8b60fc7649..55e84f20d3 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -103,7 +103,6 @@ struct ChatInfoView: View { @State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - @AppStorage(GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED, store: groupDefaults) private var pqExperimentalEnabled = false enum ChatInfoViewAlert: Identifiable { case clearChatAlert @@ -111,7 +110,6 @@ struct ChatInfoView: View { case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert - case allowContactPQEncryptionAlert case error(title: LocalizedStringKey, error: LocalizedStringKey = "") var id: String { @@ -121,7 +119,6 @@ struct ChatInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" - case .allowContactPQEncryptionAlert: return "allowContactPQEncryptionAlert" case let .error(title, _): return "error \(title)" } } @@ -168,19 +165,9 @@ struct ChatInfoView: View { } .disabled(!contact.ready || !contact.active) - if pqExperimentalEnabled, - let conn = contact.activeConn { + if let conn = contact.activeConn { Section { infoRow(Text(String("E2E encryption")), conn.connPQEnabled ? "Quantum resistant" : "Standard") - if !conn.pqEncryption { - allowPQButton() - } - } header: { - Text(String("Quantum resistant E2E encryption")) - } footer: { - if !conn.pqEncryption { - Text(String("After allowing quantum resistant encryption, it will be enabled after several messages if your contact also allows it.")) - } } } @@ -256,7 +243,6 @@ struct ChatInfoView: View { case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) }) - case .allowContactPQEncryptionAlert: return allowContactPQEncryptionAlert() case let .error(title, error): return mkAlert(title: title, message: error) } } @@ -285,8 +271,7 @@ struct ChatInfoView: View { private func contactInfoHeader() -> some View { VStack { let cInfo = chat.chatInfo - ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 192, height: 192) + ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() if contact.verified { @@ -430,15 +415,6 @@ struct ChatInfoView: View { } } - private func allowPQButton() -> some View { - Button { - alert = .allowContactPQEncryptionAlert - } label: { - Label(String("Allow PQ encryption"), systemImage: "exclamationmark.triangle") - .foregroundColor(.orange) - } - } - private func networkStatusRow() -> some View { HStack { Text("Network status") @@ -572,34 +548,6 @@ struct ChatInfoView: View { } } } - - private func allowContactPQEncryption() { - Task { - do { - let ct = try await apiSetContactPQ(contact.apiId, true) - contact = ct - await MainActor.run { - chatModel.updateContact(ct) - dismiss() - } - } catch let error { - logger.error("allowContactPQEncryption apiSetContactPQ error: \(responseError(error))") - let a = getErrorAlert(error, "Error allowing contact PQ encryption") - await MainActor.run { - alert = .error(title: a.title, error: a.message) - } - } - } - } - - func allowContactPQEncryptionAlert() -> Alert { - Alert( - title: Text(String("Allow quantum resistant encryption?")), - message: Text(String("This is an experimental feature, it is not recommended to enable it for important chats.")), - primaryButton: .destructive(Text(String("Allow")), action: allowContactPQEncryption), - secondaryButton: .cancel() - ) - } } func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift index f0bf43dffe..e3913431f5 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift @@ -35,9 +35,7 @@ struct CICallItemView: View { case .error: missedCallIcon(sent).foregroundColor(.orange) } - chatItem.timestampText - .font(.caption) - .foregroundColor(.secondary) + CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false) .padding(.bottom, 8) .padding(.horizontal, 12) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift index 03afa30331..5c9ea0f6d8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CIChatFeatureView: View { @EnvironmentObject var m: ChatModel + @ObservedObject var chat: Chat var chatItem: ChatItem @Binding var revealed: Bool var feature: Feature @@ -18,7 +19,7 @@ struct CIChatFeatureView: View { var iconColor: Color var body: some View { - if !revealed, let fs = mergedFeautures() { + if !revealed, let fs = mergedFeatures() { HStack { ForEach(fs, content: featureIconView) } @@ -47,7 +48,7 @@ struct CIChatFeatureView: View { } } - private func mergedFeautures() -> [FeatureInfo]? { + private func mergedFeatures() -> [FeatureInfo]? { var fs: [FeatureInfo] = [] var icons: Set = [] if var i = m.getChatItemIndex(chatItem) { @@ -67,8 +68,8 @@ struct CIChatFeatureView: View { switch ci.content { case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param) case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param) - case let .rcvGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param) - case let .sndGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param) + case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param) + case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param) default: nil } } @@ -103,6 +104,6 @@ struct CIChatFeatureView: View { struct CIChatFeatureView_Previews: PreviewProvider { static var previews: some View { let enabled = FeatureEnabled(forUser: false, forContact: false) - CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor) + CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index b6be3f837e..1375b87a5a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift @@ -17,6 +17,7 @@ struct CIEventView: View { .padding(.horizontal, 6) .padding(.vertical, 4) .textSelection(.disabled) + .lineLimit(4) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index c94ba3f830..ae9e09b138 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -69,19 +69,12 @@ struct CIFileView: View { return false } - private func fileSizeValid() -> Bool { - if let file = file { - return file.fileSize <= getMaxFileSize(file.fileProtocol) - } - return false - } - private func fileAction() { logger.debug("CIFileView fileAction") if let file = file { switch (file.fileStatus) { case .rcvInvitation: - if fileSizeValid() { + if fileSizeValid(file) { Task { logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") if let user = m.currentUser { @@ -143,7 +136,7 @@ struct CIFileView: View { case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvInvitation: - if fileSizeValid() { + if fileSizeValid(file) { fileIcon("arrow.down.doc.fill", color: .accentColor) } else { fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12) @@ -201,6 +194,13 @@ struct CIFileView: View { } } +func fileSizeValid(_ file: CIFile?) -> Bool { + if let file = file { + return file.fileSize <= getMaxFileSize(file.fileProtocol) + } + return false +} + func saveCryptoFile(_ fileSource: CryptoFile) { if let cfArgs = fileSource.cryptoArgs { let url = getAppFilePath(fileSource.filePath) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 72013877ca..1c9df5fcbf 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct CIGroupInvitationView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme + @ObservedObject var chat: Chat var chatItem: ChatItem var groupInvitation: CIGroupInvitation var memberRole: GroupMemberRole @@ -37,16 +38,22 @@ struct CIGroupInvitationView: View { VStack(alignment: .leading, spacing: 2) { groupInvitationText() .overlay(DetermineWidth()) - Text(chatIncognito ? "Tap to join incognito" : "Tap to join") - .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) - .font(.callout) - .padding(.trailing, 60) - .overlay(DetermineWidth()) + ( + Text(chatIncognito ? "Tap to join incognito" : "Tap to join") + .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) + .font(.callout) + + Text(" ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + ) + .overlay(DetermineWidth()) } } else { - groupInvitationText() - .padding(.trailing, 60) - .overlay(DetermineWidth()) + ( + groupInvitationText() + + Text(" ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + ) + .overlay(DetermineWidth()) } } .padding(.bottom, 2) @@ -56,9 +63,7 @@ struct CIGroupInvitationView: View { } } - chatItem.timestampText - .font(.caption) - .foregroundColor(.secondary) + CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false) } .padding(.horizontal, 12) .padding(.vertical, 6) @@ -100,9 +105,9 @@ struct CIGroupInvitationView: View { ProfileImage( imageStr: groupInvitation.groupProfile.image, iconName: "person.2.circle.fill", + size: 44, color: color ) - .frame(width: 44, height: 44) .padding(.trailing, 4) VStack(alignment: .leading) { let p = groupInvitation.groupProfile @@ -115,7 +120,7 @@ struct CIGroupInvitationView: View { } } - private func groupInvitationText() -> some View { + private func groupInvitationText() -> Text { Text(groupInvitationStr()) .font(.callout) } @@ -137,8 +142,8 @@ struct CIGroupInvitationView: View { struct CIGroupInvitationView_Previews: PreviewProvider { static var previews: some View { Group { - CIGroupInvitationView(chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(groupProfile: GroupProfile(displayName: "team", fullName: "team")), memberRole: .admin) - CIGroupInvitationView(chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(status: .accepted), memberRole: .admin) + CIGroupInvitationView(chat: Chat.sampleData, chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(groupProfile: GroupProfile(displayName: "team", fullName: "team")), memberRole: .admin) + CIGroupInvitationView(chat: Chat.sampleData, chatItem: ChatItem.getGroupInvitationSample(), groupInvitation: CIGroupInvitation.getSample(status: .accepted), memberRole: .admin) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index c189abde24..17b93930fe 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -14,6 +14,8 @@ struct CIMetaView: View { var chatItem: ChatItem var metaColor = Color.secondary var paleMetaColor = Color(UIColor.tertiaryLabel) + var showStatus = true + var showEdited = true var body: some View { if chatItem.isDeletedContent { @@ -25,24 +27,24 @@ struct CIMetaView: View { switch meta.itemStatus { case let .sndSent(sndProgress): switch sndProgress { - case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent) - case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent) + case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) + case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) } case let .sndRcvd(_, sndProgress): switch sndProgress { case .complete: ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) } case .partial: ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) } } default: - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited) } } } @@ -54,9 +56,18 @@ enum SentCheckmark { case rcvd2 } -func ciMetaText(_ meta: CIMeta, chatTTL: Int?, encrypted: Bool?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text { +func ciMetaText( + _ meta: CIMeta, + chatTTL: Int?, + encrypted: Bool?, + color: Color = .clear, + transparent: Bool = false, + sent: SentCheckmark? = nil, + showStatus: Bool = true, + showEdited: Bool = true +) -> Text { var r = Text("") - if meta.itemEdited { + if showEdited, meta.itemEdited { r = r + statusIconText("pencil", color) } if meta.disappearing { @@ -67,19 +78,21 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, encrypted: Bool?, color: Color = } r = r + Text(" ") } - if let (icon, statusColor) = meta.statusIcon(color) { - let t = Text(Image(systemName: icon)).font(.caption2) - let gap = Text(" ").kerning(-1.25) - let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) - switch sent { - case nil: r = r + t1 - case .sent: r = r + t1 + gap - case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap - case .rcvd2: r = r + gap + t1 + if showStatus { + if let (icon, statusColor) = meta.statusIcon(color) { + let t = Text(Image(systemName: icon)).font(.caption2) + let gap = Text(" ").kerning(-1.25) + let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + switch sent { + case nil: r = r + t1 + case .sent: r = r + t1 + gap + case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap + case .rcvd2: r = r + gap + t1 + } + r = r + Text(" ") + } else if !meta.disappearing { + r = r + statusIconText("circlebadge.fill", .clear) + Text(" ") } - r = r + Text(" ") - } else if !meta.disappearing { - r = r + statusIconText("circlebadge.fill", .clear) + Text(" ") } if let enc = encrypted { r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ") diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index a3918e17bc..b4b190a43a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -126,7 +126,7 @@ struct CIVideoView: View { if !decryptionInProgress { Button { decrypt(file: file) { - if let decrypted = urlDecrypted { + if urlDecrypted != nil { videoPlaying = true player?.play() } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 3475e7a8b6..95c3347f90 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -65,6 +65,8 @@ struct FramedItemView: View { } } } + } else if let itemForwarded = chatItem.meta.itemForwarded { + framedItemHeader(icon: "arrowshape.turn.up.forward", caption: Text(itemForwarded.text(chat.chatInfo.chatType)).italic(), pad: true) } ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: framedMsgContentView) @@ -163,7 +165,7 @@ struct FramedItemView: View { ) } - @ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text) -> some View { + @ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View { let v = HStack(spacing: 6) { if let icon = icon { Image(systemName: icon) @@ -178,7 +180,7 @@ struct FramedItemView: View { .foregroundColor(.secondary) .padding(.horizontal, 12) .padding(.top, 6) - .padding(.bottom, chatItem.quotedItem == nil ? 6 : 0) // TODO think how to regroup + .padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0) .overlay(DetermineWidth()) .frame(minWidth: msgWidth, alignment: .leading) .background(chatItemFrameContextColor(chatItem, colorScheme)) @@ -246,7 +248,10 @@ struct FramedItemView: View { Group { if let sender = qi.getSender(membership()) { VStack(alignment: .leading, spacing: 2) { - Text(sender).font(.caption).foregroundColor(.secondary) + Text(sender) + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) ciQuotedMsgTextView(qi, lines: 2) } } else { @@ -353,9 +358,9 @@ private struct MetaColorPreferenceKey: PreferenceKey { func onlyImageOrVideo(_ ci: ChatItem) -> Bool { if case let .image(text, _) = ci.content.msgContent { - return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == "" + return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == "" } else if case let .video(text, _, _) = ci.content.msgContent { - return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == "" + return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == "" } return false } diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift new file mode 100644 index 0000000000..7237711a2a --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -0,0 +1,151 @@ +// +// ChatItemForwardingView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 12.04.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ChatItemForwardingView: View { + @EnvironmentObject var chatModel: ChatModel + @Environment(\.dismiss) var dismiss + + var ci: ChatItem + var fromChatInfo: ChatInfo + @Binding var composeState: ComposeState + + @State private var searchText: String = "" + @FocusState private var searchFocused + + var body: some View { + NavigationView { + forwardListView() + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .principal) { + Text("Forward") + .bold() + } + } + } + } + + @ViewBuilder private func forwardListView() -> some View { + VStack(alignment: .leading) { + let chatsToForwardTo = filterChatsToForwardTo() + if !chatsToForwardTo.isEmpty { + ScrollView { + LazyVStack(alignment: .leading, spacing: 8) { + searchFieldView(text: $searchText, focussed: $searchFocused) + .padding(.leading, 2) + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) } + ForEach(chats) { chat in + Divider() + forwardListNavLinkView(chat) + .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) + } + } + .padding(.horizontal) + .padding(.vertical, 8) + .background(Color(uiColor: .systemBackground)) + .cornerRadius(12) + .padding(.horizontal) + } + .background(Color(.systemGroupedBackground)) + } else { + emptyList() + } + } + } + + private func filterChatsToForwardTo() -> [Chat] { + var filteredChats = chatModel.chats.filter({ canForwardToChat($0) }) + if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) { + let privateNotes = filteredChats.remove(at: index) + filteredChats.insert(privateNotes, at: 0) + } + return filteredChats + } + + private func filterChatSearched(_ chat: Chat, _ searchStr: String) -> Bool { + let cInfo = chat.chatInfo + return switch cInfo { + case let .direct(contact): + viewNameContains(cInfo, searchStr) || + contact.profile.displayName.localizedLowercase.contains(searchStr) || + contact.fullName.localizedLowercase.contains(searchStr) + default: + viewNameContains(cInfo, searchStr) + } + + func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { + cInfo.chatViewName.localizedLowercase.contains(s) + } + } + + private func canForwardToChat(_ chat: Chat) -> Bool { + switch chat.chatInfo { + case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv + case let .group(groupInfo): groupInfo.sendMsgEnabled + case let .local(noteFolder): noteFolder.sendMsgEnabled + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } + } + + private func emptyList() -> some View { + Text("No filtered chats") + .foregroundColor(.secondary) + .frame(maxWidth: .infinity) + } + + @ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View { + Button { + dismiss() + if chat.id == fromChatInfo.id { + composeState = ComposeState( + message: composeState.message, + preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, + contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo) + ) + } else { + composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo) + chatModel.chatId = chat.id + } + } label: { + HStack { + ChatInfoImage(chat: chat, size: 30) + .padding(.trailing, 2) + Text(chat.chatInfo.chatViewName) + .foregroundColor(.primary) + .lineLimit(1) + if chat.chatInfo.incognito { + Spacer() + Image(systemName: "theatermasks") + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +#Preview { + ChatItemForwardingView( + ci: ChatItem.getSample(1, .directSnd, .now, "hello"), + fromChatInfo: .direct(contact: Contact.sampleData), + composeState: Binding.constant(ComposeState(message: "hello")) + ) +} diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 8dd43cc01b..19aa261396 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct ChatItemInfoView: View { @EnvironmentObject var chatModel: ChatModel + @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme var ci: ChatItem @Binding var chatItemInfo: ChatItemInfo? @@ -21,6 +22,7 @@ struct ChatItemInfoView: View { enum CIInfoTab { case history case quote + case forwarded case delivery } @@ -68,9 +70,20 @@ struct ChatItemInfoView: View { if ci.quotedItem != nil { numTabs += 1 } + if chatItemInfo?.forwardedFromChatItem != nil { + numTabs += 1 + } return numTabs } + private var local: Bool { + switch ci.chatDir { + case .localSnd: true + case .localRcv: true + default: false + } + } + @ViewBuilder private func itemInfoView() -> some View { if numTabs > 1 { TabView(selection: $selection) { @@ -93,6 +106,13 @@ struct ChatItemInfoView: View { } .tag(CIInfoTab.quote) } + if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem { + forwardedFromTab(forwardedFromItem) + .tabItem { + Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward") + } + .tag(CIInfoTab.forwarded) + } } .onAppear { if chatItemInfo?.memberDeliveryStatuses != nil { @@ -275,6 +295,74 @@ struct ChatItemInfoView: View { : Color(uiColor: .tertiarySystemGroupedBackground) } + @ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + details() + Divider().padding(.vertical) + Text(local ? "Saved from" : "Forwarded from") + .font(.title2) + .padding(.bottom, 4) + forwardedFromView(forwardedFromItem) + } + .padding() + } + .frame(maxHeight: .infinity, alignment: .top) + } + + private func forwardedFromView(_ forwardedFromItem: AChatItem) -> some View { + VStack(alignment: .leading, spacing: 8) { + Button { + Task { + await MainActor.run { + chatModel.chatId = forwardedFromItem.chatInfo.id + dismiss() + } + } + } label: { + forwardedFromSender(forwardedFromItem) + } + + if !local { + Divider().padding(.top, 32) + Text("Recipient(s) can't see who this message is from.") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + + @ViewBuilder private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View { + HStack { + ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo), size: 48) + .padding(.trailing, 6) + + if forwardedFromItem.chatItem.chatDir.sent { + VStack(alignment: .leading) { + Text("you") + .italic() + .foregroundColor(.primary) + Text(forwardedFromItem.chatInfo.chatViewName) + .foregroundColor(.secondary) + .lineLimit(1) + } + } else if case let .groupRcv(groupMember) = forwardedFromItem.chatItem.chatDir { + VStack(alignment: .leading) { + Text(groupMember.chatViewName) + .foregroundColor(.primary) + .lineLimit(1) + Text(forwardedFromItem.chatInfo.chatViewName) + .foregroundColor(.secondary) + .lineLimit(1) + } + } else { + Text(forwardedFromItem.chatInfo.chatViewName) + .foregroundColor(.primary) + .lineLimit(1) + } + } + } + @ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { ScrollView { VStack(alignment: .leading, spacing: 16) { @@ -316,8 +404,7 @@ struct ChatItemInfoView: View { private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View { HStack{ - ProfileImage(imageStr: member.image) - .frame(width: 30, height: 30) + ProfileImage(imageStr: member.image, size: 30) .padding(.trailing, 2) Text(member.chatViewName) .lineLimit(1) diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index da9dc523e1..d580fb5f3e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -46,7 +46,7 @@ struct ChatItemView: View { let ci = chatItem if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) { MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed) - } else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { + } else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { EmojiItemView(chat: chat, chatItem: ci) } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { @@ -102,9 +102,9 @@ struct ChatItemContentView: View { case let .rcvChatPreference(feature, allowed, param): CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param) case let .sndChatPreference(feature, _, _): - CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary) - case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor) - case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor) + CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary) + case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor) + case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red) case .sndModerated: deletedItemView() @@ -127,7 +127,7 @@ struct ChatItemContentView: View { } private func groupInvitationItemView(_ groupInvitation: CIGroupInvitation, _ memberRole: GroupMemberRole) -> some View { - CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito) + CIGroupInvitationView(chat: chat, chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chat.chatInfo.incognito) } private func eventItemView() -> some View { @@ -149,7 +149,7 @@ struct ChatItemContentView: View { } private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { - CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor) + CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor) } private var mergedGroupEventText: Text? { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index cd2aa55bc3..4055ca2b28 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -83,7 +83,11 @@ struct ChatView: View { initChatView() } .onChange(of: chatModel.chatId) { cId in - if cId != nil { + showChatInfoSheet = false + if let cId { + if let c = chatModel.getChat(cId) { + chat = c + } initChatView() } else { dismiss() @@ -179,7 +183,7 @@ struct ChatView: View { .disabled(!contact.ready || !contact.active) } searchButton() - toggleNtfsButton(chat) + ToggleNtfsButton(chat: chat) .disabled(!contact.ready || !contact.active) } label: { Image(systemName: "ellipsis") @@ -208,7 +212,7 @@ struct ChatView: View { } Menu { searchButton() - toggleNtfsButton(chat) + ToggleNtfsButton(chat: chat) } label: { Image(systemName: "ellipsis") } @@ -251,7 +255,8 @@ struct ChatView: View { } } } - if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft { + if chatModel.draftChatId == cInfo.id && !composeState.forwarding, + let draft = chatModel.draft { composeState = draft } if chat.chatStats.unreadChat { @@ -296,7 +301,7 @@ struct ChatView: View { } private func voiceWithoutFrame(_ ci: ChatItem) -> Bool { - ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil + ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil } private func chatItemsList() -> some View { @@ -342,8 +347,8 @@ struct ChatView: View { .onChange(of: searchText) { _ in loadChat(chat: chat, search: searchText) } - .onChange(of: chatModel.chatId) { _ in - if let chatId = chatModel.chatId, let c = chatModel.getChat(chatId) { + .onChange(of: chatModel.chatId) { chatId in + if let chatId, let c = chatModel.getChat(chatId) { chat = c showChatInfoSheet = false loadChat(chat: c) @@ -539,6 +544,7 @@ struct ChatView: View { @State private var revealed = false @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? + @State private var showForwardingSheet: Bool = false @State private var allowMenu: Bool = true @@ -587,12 +593,12 @@ struct ChatView: View { Text(memberNames(member, prevMember, memCount)) .font(.caption) .foregroundStyle(.secondary) + .lineLimit(2) .padding(.leading, memberImageSize + 14) .padding(.top, 7) } HStack(alignment: .top, spacing: 8) { - ProfileImage(imageStr: member.memberProfile.image) - .frame(width: memberImageSize, height: memberImageSize) + ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize) .onTapGesture { if chatView.membersLoaded { selectedMember = m.getGroupMember(member.groupMemberId) @@ -667,7 +673,7 @@ struct ChatView: View { Button("Delete for me", role: .destructive) { deleteMessage(.cidmInternal) } - if let di = deletingItem, di.meta.editable && !di.localNote { + if let di = deletingItem, di.meta.deletable && !di.localNote { Button(broadcastDeleteButtonText, role: .destructive) { deleteMessage(.cidmBroadcast) } @@ -693,6 +699,14 @@ struct ChatView: View { }) { ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo) } + .sheet(isPresented: $showForwardingSheet) { + if #available(iOS 16.0, *) { + ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState) + .presentationDetents([.fraction(0.8)]) + } else { + ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState) + } + } } private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool { @@ -771,10 +785,17 @@ struct ChatView: View { } else { menu.append(saveFileAction(fileSource)) } + } else if let file = ci.file, case .rcvInvitation = file.fileStatus, fileSizeValid(file) { + menu.append(downloadFileAction(file)) } if ci.meta.editable && !mc.isVoice && !live { menu.append(editAction(ci)) } + if ci.meta.itemDeleted == nil + && (ci.file == nil || (fileSource != nil && fileExists)) + && !ci.isLiveDummy && !live { + menu.append(forwardUIAction(ci)) + } if !ci.isLiveDummy { menu.append(viewInfoUIAction(ci)) } @@ -807,6 +828,9 @@ struct ChatView: View { menu.append(deleteUIAction(ci)) } else if ci.mergeCategory != nil && ((range?.count ?? 0) > 1 || revealed) { menu.append(revealed ? shrinkUIAction() : expandUIAction()) + menu.append(deleteUIAction(ci)) + } else if ci.showLocalDelete { + menu.append(deleteUIAction(ci)) } return menu } @@ -826,6 +850,15 @@ struct ChatView: View { } } + private func forwardUIAction(_ ci: ChatItem) -> UIAction { + UIAction( + title: NSLocalizedString("Forward", comment: "chat item action"), + image: UIImage(systemName: "arrowshape.turn.up.forward") + ) { _ in + showForwardingSheet = true + } + } + private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu { UIMenu( title: NSLocalizedString("React…", comment: "chat item menu"), @@ -923,7 +956,21 @@ struct ChatView: View { saveCryptoFile(fileSource) } } - + + private func downloadFileAction(_ file: CIFile) -> UIAction { + UIAction( + title: NSLocalizedString("Download", comment: "chat item action"), + image: UIImage(systemName: "arrow.down.doc") + ) { _ in + Task { + logger.debug("ChatView downloadFileAction, in Task") + if let user = m.currentUser { + await receiveFile(user: user, fileId: file.fileId) + } + } + } + } + private func editAction(_ ci: ChatItem) -> UIAction { UIAction( title: NSLocalizedString("Edit", comment: "chat item action"), @@ -996,7 +1043,7 @@ struct ChatView: View { image: UIImage(systemName: "trash"), attributes: [.destructive] ) { _ in - if !revealed && ci.meta.itemDeleted != nil, + if !revealed, let currIndex = m.getChatItemIndex(ci), let ciCategory = ci.mergeCategory { let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory) @@ -1172,14 +1219,18 @@ struct ChatView: View { } } -@ViewBuilder func toggleNtfsButton(_ chat: Chat) -> some View { - Button { - toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) - } label: { - if chat.chatInfo.ntfsEnabled { - Label("Mute", systemImage: "speaker.slash") - } else { - Label("Unmute", systemImage: "speaker.wave.2") +struct ToggleNtfsButton: View { + @ObservedObject var chat: Chat + + var body: some View { + Button { + toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) + } label: { + if chat.chatInfo.ntfsEnabled { + Label("Mute", systemImage: "speaker.slash") + } else { + Label("Unmute", systemImage: "speaker.wave.2") + } } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 604e0a276d..6cf9df782b 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -23,6 +23,7 @@ enum ComposeContextItem { case noContextItem case quotedItem(chatItem: ChatItem) case editingItem(chatItem: ChatItem) + case forwardingItem(chatItem: ChatItem, fromChatInfo: ChatInfo) } enum VoiceMessageRecordingState { @@ -72,6 +73,13 @@ struct ComposeState { } } + init(forwardingItem: ChatItem, fromChatInfo: ChatInfo) { + self.message = "" + self.preview = .noPreview + self.contextItem = .forwardingItem(chatItem: forwardingItem, fromChatInfo: fromChatInfo) + self.voiceMessageRecordingState = .noRecording + } + func copy( message: String? = nil, liveMessage: LiveMessage? = nil, @@ -102,12 +110,19 @@ struct ComposeState { } } + var forwarding: Bool { + switch contextItem { + case .forwardingItem: return true + default: return false + } + } + var sendEnabled: Bool { switch preview { case let .mediaPreviews(media): return !media.isEmpty case .voicePreview: return voiceMessageRecordingState == .finished case .filePreview: return true - default: return !message.isEmpty || liveMessage != nil + default: return !message.isEmpty || forwarding || liveMessage != nil } } @@ -153,7 +168,7 @@ struct ComposeState { } var attachmentDisabled: Bool { - if editing || liveMessage != nil || inProgress { return true } + if editing || forwarding || liveMessage != nil || inProgress { return true } switch preview { case .noPreview: return false case .linkPreview: return false @@ -161,6 +176,16 @@ struct ComposeState { } } + var attachmentPreview: Bool { + switch preview { + case .noPreview: false + case .linkPreview: false + case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty + case .voicePreview: false + case .filePreview: true + } + } + var empty: Bool { message == "" && noPreview } @@ -234,6 +259,7 @@ struct ComposeView: View { @Binding var keyboardVisible: Bool @State var linkUrl: URL? = nil + @State var hasSimplexLink: Bool = false @State var prevLinkUrl: URL? = nil @State var pendingLinkUrl: URL? = nil @State var cancelledLinks: Set = [] @@ -260,6 +286,16 @@ struct ComposeView: View { if chat.chatInfo.contact?.nextSendGrpInv ?? false { ContextInvitingContactMemberView() } + let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) + let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files) + let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice) + if simplexLinkProhibited { + msgNotAllowedView("SimpleX links not allowed", icon: "link") + } else if fileProhibited { + msgNotAllowedView("Files and media not allowed", icon: "doc") + } else if voiceProhibited { + msgNotAllowedView("Voice messages not allowed", icon: "mic") + } contextItemView() switch (composeState.editing, composeState.preview) { case (true, .filePreview): EmptyView() @@ -278,7 +314,7 @@ struct ComposeView: View { .padding(.bottom, 12) .padding(.leading, 12) if case let .group(g) = chat.chatInfo, - !g.fullGroupPreferences.files.on { + !g.fullGroupPreferences.files.on(for: g.membership) { b.disabled(true).onTapGesture { AlertManager.shared.showAlertMsg( title: "Files and media prohibited!", @@ -303,6 +339,7 @@ struct ComposeView: View { }, nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false, voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice), + disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited, showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert, startVoiceMessageRecording: { Task { @@ -337,13 +374,18 @@ struct ComposeView: View { } } } - .onChange(of: composeState.message) { _ in + .onChange(of: composeState.message) { msg in if composeState.linkPreviewAllowed { - if composeState.message.count > 0 { - showLinkPreview(composeState.message) + if msg.count > 0 { + showLinkPreview(msg) } else { resetLinkPreview() + hasSimplexLink = false } + } else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) { + (_, hasSimplexLink) = parseMessage(msg) + } else { + hasSimplexLink = false } } .onChange(of: chat.userCanSend) { canSend in @@ -610,6 +652,18 @@ struct ComposeView: View { } } + private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View { + HStack { + Image(systemName: icon).foregroundColor(.secondary) + Text(reason).italic() + } + .padding(12) + .frame(minHeight: 50) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(uiColor: .tertiarySystemGroupedBackground)) + .padding(.top, 8) + } + @ViewBuilder private func contextItemView() -> some View { switch composeState.contextItem { case .noContextItem: @@ -628,6 +682,14 @@ struct ComposeView: View { contextIcon: "pencil", cancelContextItem: { clearState() } ) + case let .forwardingItem(chatItem: forwardedItem, _): + ContextItemView( + chat: chat, + contextItem: forwardedItem, + contextIcon: "arrowshape.turn.up.forward", + cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }, + showSender: false + ) } } @@ -649,6 +711,11 @@ struct ComposeView: View { } if chat.chatInfo.contact?.nextSendGrpInv ?? false { await sendMemberContactInvitation() + } else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem { + sent = await forwardItem(ci, fromChatInfo) + if !composeState.message.isEmpty { + sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil) + } } else if case let .editingItem(ci) = composeState.contextItem { sent = await updateMessage(ci, live: live) } else if let liveMessage = liveMessage, liveMessage.sentMsg != nil { @@ -694,7 +761,15 @@ struct ComposeView: View { } } } - await MainActor.run { clearState(live: live) } + await MainActor.run { + let wasForwarding = composeState.forwarding + clearState(live: live) + if wasForwarding, + chatModel.draftChatId == chat.chatInfo.id, + let draft = chatModel.draft { + composeState = draft + } + } return sent func sending() async { @@ -815,10 +890,26 @@ struct ComposeView: View { return nil } + func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? { + if let chatItem = await apiForwardChatItem( + toChatType: chat.chatInfo.chatType, + toChatId: chat.chatInfo.apiId, + fromChatType: fromChatInfo.chatType, + fromChatId: fromChatInfo.apiId, + itemId: forwardedItem.id + ) { + await MainActor.run { + chatModel.addChatItem(chat.chatInfo, chatItem) + } + return chatItem + } + return nil + } + func checkLinkPreview() -> MsgContent { switch (composeState.preview) { case let .linkPreview(linkPreview: linkPreview): - if let url = parseMessage(msgText), + if let url = parseMessage(msgText).url, let linkPreview = linkPreview, url == linkPreview.uri { return .link(text: msgText, preview: linkPreview) @@ -947,7 +1038,7 @@ struct ComposeView: View { private func showLinkPreview(_ s: String) { prevLinkUrl = linkUrl - linkUrl = parseMessage(s) + (linkUrl, hasSimplexLink) = parseMessage(s) if let url = linkUrl { if url != composeState.linkPreview?.uri && url != pendingLinkUrl { pendingLinkUrl = url @@ -964,13 +1055,17 @@ struct ComposeView: View { } } - private func parseMessage(_ msg: String) -> URL? { - let parsedMsg = parseSimpleXMarkdown(msg) - let uri = parsedMsg?.first(where: { ft in + private func parseMessage(_ msg: String) -> (url: URL?, hasSimplexLink: Bool) { + guard let parsedMsg = parseSimpleXMarkdown(msg) else { return (nil, false) } + let url: URL? = if let uri = parsedMsg.first(where: { ft in ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) - }) - if let uri = uri { return URL(string: uri.text) } - else { return nil } + }) { + URL(string: uri.text) + } else { + nil + } + let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) + return (url, simplexLink) } private func isSimplexLink(_ link: String) -> Bool { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 3eb128cded..2777d8321c 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -15,6 +15,7 @@ struct ContextItemView: View { let contextItem: ChatItem let contextIcon: String let cancelContextItem: () -> Void + var showSender: Bool = true var body: some View { HStack { @@ -23,7 +24,7 @@ struct ContextItemView: View { .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) .foregroundColor(.secondary) - if let sender = contextItem.memberDisplayName { + if showSender, let sender = contextItem.memberDisplayName { VStack(alignment: .leading, spacing: 4) { Text(sender).font(.caption).foregroundColor(.secondary) msgContentView(lines: 2) @@ -48,14 +49,26 @@ struct ContextItemView: View { } private func msgContentView(lines: Int) -> some View { - MsgContentView( - chat: chat, - text: contextItem.text, - formattedText: contextItem.formattedText, - showSecrets: false - ) - .multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading) - .lineLimit(lines) + contextMsgPreview() + .multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading) + .lineLimit(lines) + } + + private func contextMsgPreview() -> Text { + return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false) + + func attachment() -> Text { + switch contextItem.content.msgContent { + case .file: return image("doc.fill") + case .image: return image("photo") + case .voice: return image("play.fill") + default: return Text("") + } + } + + func image(_ s: String) -> Text { + Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index 3eead5b0af..f2c7221835 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -16,7 +16,6 @@ struct NativeTextEditor: UIViewRepresentable { @Binding var disableEditing: Bool @Binding var height: CGFloat @Binding var focused: Bool - let alignment: TextAlignment let onImagesAdded: ([UploadContent]) -> Void private let minHeight: CGFloat = 37 @@ -30,13 +29,16 @@ struct NativeTextEditor: UIViewRepresentable { func makeUIView(context: Context) -> UITextView { let field = CustomUITextField(height: _height) field.text = text - field.textAlignment = alignment == .leading ? .left : .right + field.textAlignment = alignment(text) field.autocapitalizationType = .sentences field.setOnTextChangedListener { newText, images in if !disableEditing { - // Speed up the process of updating layout, reduce jumping content on screen - if !isShortEmoji(newText) { updateHeight(field) } text = newText + field.textAlignment = alignment(text) + updateFont(field) + // Speed up the process of updating layout, reduce jumping content on screen + updateHeight(field) + self.height = field.frame.size.height } else { field.text = text } @@ -53,10 +55,12 @@ struct NativeTextEditor: UIViewRepresentable { } func updateUIView(_ field: UITextView, context: Context) { - field.text = text - field.textAlignment = alignment == .leading ? .left : .right - updateFont(field) - updateHeight(field) + if field.markedTextRange == nil && field.text != text { + field.text = text + field.textAlignment = alignment(text) + updateFont(field) + updateHeight(field) + } } private func updateHeight(_ field: UITextView) { @@ -73,12 +77,19 @@ struct NativeTextEditor: UIViewRepresentable { } private func updateFont(_ field: UITextView) { - field.font = isShortEmoji(field.text) + let newFont = isShortEmoji(field.text) ? (field.text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont) : UIFont.preferredFont(forTextStyle: .body) + if field.font != newFont { + field.font = newFont + } } } +private func alignment(_ text: String) -> NSTextAlignment { + isRightToLeft(text) ? .right : .left +} + private class CustomUITextField: UITextView, UITextViewDelegate { var height: Binding var newHeight: CGFloat = 0 @@ -205,7 +216,6 @@ struct NativeTextEditor_Previews: PreviewProvider{ disableEditing: Binding.constant(false), height: Binding.constant(100), focused: Binding.constant(false), - alignment: TextAlignment.leading, onImagesAdded: { _ in } ) .fixedSize(horizontal: false, vertical: true) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 8f7b23c888..8b528a201c 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -20,6 +20,7 @@ struct SendMessageView: View { var nextSendGrpInv: Bool = false var showVoiceMessageButton: Bool = true var voiceMessageAllowed: Bool = true + var disableSendButton = false var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other var startVoiceMessageRecording: (() -> Void)? = nil var finishVoiceMessageRecording: (() -> Void)? = nil @@ -53,13 +54,11 @@ struct SendMessageView: View { .padding(.vertical, 8) .frame(maxWidth: .infinity) } else { - let alignment: TextAlignment = isRightToLeft(composeState.message) ? .trailing : .leading NativeTextEditor( text: $composeState.message, disableEditing: $composeState.inProgress, height: $teHeight, focused: $keyboardVisible, - alignment: alignment, onImagesAdded: onMediaAdded ) .allowsTightening(false) @@ -109,6 +108,7 @@ struct SendMessageView: View { } else if showVoiceMessageButton && composeState.message.isEmpty && !composeState.editing + && !composeState.forwarding && composeState.liveMessage == nil && ((composeState.noPreview && vmrs == .noRecording) || (vmrs == .recording && holdingVMR)) { @@ -184,7 +184,8 @@ struct SendMessageView: View { !composeState.sendEnabled || composeState.inProgress || (!voiceMessageAllowed && composeState.voicePreview) || - composeState.endLiveDisabled + composeState.endLiveDisabled || + disableSendButton ) .frame(width: 29, height: 29) .contextMenu{ @@ -223,7 +224,8 @@ struct SendMessageView: View { @ViewBuilder private func sendButtonContextMenuItems() -> some View { if composeState.liveMessage == nil, - !composeState.editing { + !composeState.editing, + !composeState.forwarding { if case .noContextItem = composeState.contextItem, !composeState.voicePreview, let send = sendLiveMessage, diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index b89c006c61..ed2afb91b3 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -194,8 +194,7 @@ struct AddGroupMembersViewCommon: View { } } label: { HStack{ - ProfileImage(imageStr: contact.image) - .frame(width: 30, height: 30) + ProfileImage(imageStr: contact.image, size: 30) .padding(.trailing, 2) Text(ChatInfo.direct(contact: contact).chatViewName) .foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 88b36077b4..c22f3f0fed 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -62,7 +62,7 @@ struct GroupChatInfoView: View { NavigationView { let members = chatModel.groupMembers .filter { m in let status = m.wrapped.memberStatus; return status != .memLeft && status != .memRemoved } - .sorted { $0.displayName.lowercased() < $1.displayName.lowercased() } + .sorted { $0.wrapped.memberRole > $1.wrapped.memberRole } List { groupInfoHeader() @@ -172,8 +172,7 @@ struct GroupChatInfoView: View { private func groupInfoHeader() -> some View { VStack { let cInfo = chat.chatInfo - ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 192, height: 192) + ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() Text(cInfo.displayName) @@ -217,8 +216,7 @@ struct GroupChatInfoView: View { var body: some View { let member = groupMember.wrapped let v = HStack{ - ProfileImage(imageStr: member.image) - .frame(width: 38, height: 38) + ProfileImage(imageStr: member.image, size: 38) .padding(.trailing, 2) // TODO server connection status VStack(alignment: .leading) { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 999617dde7..a24608b7e7 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -83,7 +83,7 @@ struct GroupMemberInfoView: View { Section { if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) { knownDirectChatButton(chat) - } else if groupInfo.fullGroupPreferences.directMessages.on { + } else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { if let contactId = member.memberContactId { newDirectChatButton(contactId) } else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false { @@ -110,7 +110,7 @@ struct GroupMemberInfoView: View { Label("Share address", systemImage: "square.and.arrow.up") } if let contactId = member.memberContactId { - if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on { + if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { connectViaAddressButton(contactLink) } } else { @@ -305,8 +305,7 @@ struct GroupMemberInfoView: View { private func groupMemberInfoHeader(_ mem: GroupMember) -> some View { VStack { - ProfileImage(imageStr: mem.image, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 192, height: 192) + ProfileImage(imageStr: mem.image, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() if mem.verified { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 7ab4bf4ece..b4e1992848 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -9,6 +9,12 @@ import SwiftUI import SimpleXChat +private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] = [ + (nil, "all members"), + (.admin, "admins"), + (.owner, "owners") +] + struct GroupPreferencesView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var chatModel: ChatModel @@ -24,10 +30,12 @@ struct GroupPreferencesView: View { List { featureSection(.timedMessages, $preferences.timedMessages.enable) featureSection(.fullDelete, $preferences.fullDelete.enable) - featureSection(.directMessages, $preferences.directMessages.enable) + featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) featureSection(.reactions, $preferences.reactions.enable) - featureSection(.voice, $preferences.voice.enable) - featureSection(.files, $preferences.files.enable) + featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) + featureSection(.files, $preferences.files.enable, $preferences.files.role) + // TODO enable simplexLinks preference in 5.8 + // featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) featureSection(.history, $preferences.history.enable) if groupInfo.canEdit { @@ -64,7 +72,7 @@ struct GroupPreferencesView: View { } } - private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding) -> some View { + private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding, _ enableForRole: Binding? = nil) -> some View { Section { let color: Color = enableFeature.wrappedValue == .on ? .green : .secondary let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon @@ -87,6 +95,16 @@ struct GroupPreferencesView: View { ) .frame(height: 36) } + if enableFeature.wrappedValue == .on, let enableForRole { + Picker("Enabled for", selection: enableForRole) { + ForEach(featureRoles, id: \.role) { fr in + Text(fr.text) + } + } + .frame(height: 36) + // remove in v5.8 + .disabled(true) + } } else { settingsRow(icon, color: color) { infoRow(Text(feature.text), enableFeature.wrappedValue.text) @@ -94,10 +112,26 @@ struct GroupPreferencesView: View { if timedOn { infoRow("Delete after", timeText(preferences.timedMessages.ttl)) } + if enableFeature.wrappedValue == .on, let enableForRole { + HStack { + Text("Enabled for").foregroundColor(.secondary) + Spacer() + Text( + featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text + ?? "all members" + ) + .foregroundColor(.secondary) + } + } } } footer: { Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit)) } + .onChange(of: enableFeature.wrappedValue) { enabled in + if case .off = enabled { + enableForRole?.wrappedValue = nil + } + } } private func savePreferences() { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 7fbc1e4ac8..efe254323e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -92,7 +92,7 @@ struct ChatListNavLink: View { .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() - toggleNtfsButton(chat) + ToggleNtfsButton(chat: chat) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if !chat.chatItems.isEmpty { @@ -181,7 +181,7 @@ struct ChatListNavLink: View { .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() - toggleNtfsButton(chat) + ToggleNtfsButton(chat: chat) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if !chat.chatItems.isEmpty { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 38aabdc21d..6bf63bb2e3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -92,8 +92,7 @@ struct ChatListView: View { ToolbarItem(placement: .navigationBarLeading) { let user = chatModel.currentUser ?? User.sampleData ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel)) - .frame(width: 32, height: 32) + ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) .padding(.trailing, 4) let allRead = chatModel.users .filter { u in !u.user.activeUser && !u.user.hidden } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 8bfc8fec03..fe8fd8b28e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -23,8 +23,7 @@ struct ChatPreviewView: View { let cItem = chat.chatItems.last return HStack(spacing: 8) { ZStack(alignment: .bottomTrailing) { - ChatInfoImage(chat: chat) - .frame(width: 63, height: 63) + ChatInfoImage(chat: chat, size: 63) chatPreviewImageOverlayIcon() .padding([.bottom, .trailing], 1) } diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index c5c062a6ec..dacf51a5e8 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -16,8 +16,7 @@ struct ContactRequestView: View { var body: some View { HStack(spacing: 8) { - ChatInfoImage(chat: chat) - .frame(width: 63, height: 63) + ChatInfoImage(chat: chat, size: 63) .padding(.leading, 4) VStack(alignment: .leading, spacing: 0) { HStack(alignment: .top) { diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index 922fc84e53..a615f9c118 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -127,8 +127,7 @@ struct UserPicker: View { } }, label: { HStack(spacing: 0) { - ProfileImage(imageStr: user.image, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 44, height: 44) + ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill)) .padding(.trailing, 12) Text(user.chatViewName) .fontWeight(user.activeUser ? .medium : .regular) diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index e253cdd72c..0180b066ab 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -12,6 +12,7 @@ import SimpleXChat struct ChatInfoImage: View { @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat + var size: CGFloat var color = Color(uiColor: .tertiarySystemGroupedBackground) var body: some View { @@ -28,6 +29,7 @@ struct ChatInfoImage: View { return ProfileImage( imageStr: chat.chatInfo.image, iconName: iconName, + size: size, color: iconColor ) } @@ -36,8 +38,9 @@ struct ChatInfoImage: View { struct ChatInfoImage_Previews: PreviewProvider { static var previews: some View { ChatInfoImage( - chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) - , color: Color(red: 0.9, green: 0.9, blue: 0.9) + chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), + size: 63, + color: Color(red: 0.9, green: 0.9, blue: 0.9) ) .previewLayout(.fixed(width: 63, height: 63)) } diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index cc4f09ae3b..6b8439504a 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -9,29 +9,51 @@ import SwiftUI import SimpleXChat +let defaultProfileImageCorner = 22.5 + struct ProfileImage: View { var imageStr: String? = nil var iconName: String = "person.crop.circle.fill" + var size: CGFloat var color = Color(uiColor: .tertiarySystemGroupedBackground) - + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner + var body: some View { if let image = imageStr, let data = Data(base64Encoded: dropImagePrefix(image)), let uiImage = UIImage(data: data) { - Image(uiImage: uiImage) - .resizable() - .clipShape(Circle()) + clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) } else { Image(systemName: iconName) .resizable() .foregroundColor(color) + .frame(width: size, height: size) } } } +private let squareToCircleRatio = 0.935 + +private let radiusFactor = (1 - squareToCircleRatio) / 50 + +@ViewBuilder func clipProfileImage(_ img: Image, size: CGFloat, radius: Double) -> some View { + let v = img.resizable() + if radius >= 50 { + v.frame(width: size, height: size).clipShape(Circle()) + } else if radius <= 0 { + let sz = size * squareToCircleRatio + v.frame(width: sz, height: sz).padding((size - sz) / 2) + } else { + let sz = size * (squareToCircleRatio + radius * radiusFactor) + v.frame(width: sz, height: sz) + .clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous)) + .padding((size - sz) / 2) + } +} + struct ProfileImage_Previews: PreviewProvider { static var previews: some View { - ProfileImage(imageStr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC") + ProfileImage(imageStr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC", size: 63) .previewLayout(.fixed(width: 63, height: 63)) .background(.black) } diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index 9afd0dd406..e290537b46 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -448,6 +448,9 @@ struct MigrateToDevice: View { case .rcvFileError: alert = .error(title: "Download failed", error: "File was deleted or link is invalid") migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) + case .chatError(_, .error(.noRcvFileUser)): + alert = .error(title: "Download failed", error: "File was deleted or link is invalid") + migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) default: logger.debug("unsupported event: \(msg.responseType)") } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 3f3623033e..4b272f4caa 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -70,9 +70,7 @@ struct AddGroupView: View { ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, color: Color(uiColor: .secondarySystemGroupedBackground)) - .aspectRatio(1, contentMode: .fit) - .frame(maxWidth: 128, maxHeight: 128) + ProfileImage(imageStr: profile.image, size: 128, color: Color(uiColor: .secondarySystemGroupedBackground)) if profile.image != nil { Button { profile.image = nil diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 73ea5720c6..209c440b16 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -375,6 +375,37 @@ private let versionDescriptions: [VersionDescription] = [ description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" ), ] + ), + VersionDescription( + version: "v5.7", + post: URL(string: "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html"), + features: [ + FeatureDescription( + icon: "key", + title: "Quantum resistant encryption", + description: "Will be enabled in direct chats!" + ), + FeatureDescription( + icon: "arrowshape.turn.up.forward", + title: "Forward and save messages", + description: "Message source remains private." + ), + FeatureDescription( + icon: "music.note", + title: "In-call sounds", + description: "When connecting audio and video calls." + ), + FeatureDescription( + icon: "person.crop.square", + title: "Shape profile images", + description: "Square, circle, or anything in between." + ), + FeatureDescription( + icon: "antenna.radiowaves.left.and.right", + title: "Network management", + description: "More reliable network connection." + ) + ] ) ] diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift index 9da3bac00b..f027127db3 100644 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift @@ -51,9 +51,10 @@ struct AdvancedNetworkSettings: View { } .disabled(currentNetCfg == NetCfg.proxyDefaults) - timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel) + timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_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("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel) + intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") 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/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 1f648b09dc..b91d2c9369 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -17,11 +17,14 @@ let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"] let appSettingsURL = URL(string: UIApplication.openSettingsURLString)! struct AppearanceSettings: View { + @EnvironmentObject var m: ChatModel + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var sceneDelegate: SceneDelegate @State private var iconLightTapped = false @State private var iconDarkTapped = false @State private var userInterfaceStyle = getUserInterfaceStyleDefault() @State private var uiTintColor = getUIAccentColorDefault() + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner var body: some View { VStack{ @@ -44,6 +47,23 @@ struct AppearanceSettings: View { } } + Section("Profile images") { + HStack(spacing: 16) { + if let img = m.currentUser?.image, img != "" { + ProfileImage(imageStr: img, size: 60) + } else { + clipProfileImage(Image(colorScheme == .light ? "icon-dark" : "icon-light"), size: 60, radius: profileImageCornerRadius) + } + + Slider( + value: $profileImageCornerRadius, + in: 0...50, + step: 2.5 + ) + } + .foregroundColor(.secondary) + } + Section { Picker("Theme", selection: $userInterfaceStyle) { ForEach(interfaceStyles, id: \.self) { style in @@ -93,7 +113,7 @@ struct AppearanceSettings: View { } ._onButtonGesture { tapped.wrappedValue = $0 } perform: {} .overlay(tapped.wrappedValue ? Color.secondary : Color.clear) - .cornerRadius(20) + .cornerRadius(13.5) } } diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 9b11c6d0f7..3bbfbfe33e 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -12,7 +12,6 @@ import SimpleXChat struct DeveloperView: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false - @AppStorage(GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED, store: groupDefaults) private var pqExperimentalEnabled = false @Environment(\.colorScheme) var colorScheme var body: some View { @@ -43,33 +42,9 @@ struct DeveloperView: View { } footer: { (developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.") } - - if developerTools { - Section { - settingsRow("key") { - Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled) - .onChange(of: pqExperimentalEnabled) { - setPQExperimentalEnabled($0) - } - } - } header: { - Text(String("Experimental")) - } footer: { - Text(String("In this version applies only to new contacts.")) - } - } } } } - - private func setPQExperimentalEnabled(_ enable: Bool) { - do { - try apiSetPQEncryption(enable) - } catch let error { - let err = responseError(error) - logger.error("apiSetPQEncryption \(err)") - } - } } struct DeveloperView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index d721cfad50..a6702b1821 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -24,6 +24,7 @@ private enum NetworkAlert: Identifiable { } struct NetworkAndServers: View { + @EnvironmentObject var m: ChatModel @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var cfgLoaded = false @State private var currentNetCfg = NetCfg.defaults @@ -82,6 +83,14 @@ struct NetworkAndServers: View { Text("WebRTC ICE servers") } } + + Section("Network connection") { + HStack { + Text(m.networkInfo.networkType.text) + Spacer() + Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red) + } + } } } .onAppear { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 1799d8136a..e532448a90 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -45,6 +45,7 @@ let DEFAULT_ACCENT_COLOR_RED = "accentColorRed" let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" +let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius" let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice" @@ -87,6 +88,7 @@ let appDefaults: [String: Any] = [ DEFAULT_ACCENT_COLOR_GREEN: 0.533, DEFAULT_ACCENT_COLOR_BLUE: 1.000, DEFAULT_USER_INTERFACE_STYLE: 0, + DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner, DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true, @@ -425,8 +427,7 @@ struct ProfilePreview: View { var body: some View { HStack { - ProfileImage(imageStr: profileOf.image, color: color) - .frame(width: 44, height: 44) + ProfileImage(imageStr: profileOf.image, size: 44, color: color) .padding(.trailing, 6) .padding(.vertical, 6) VStack(alignment: .leading) { diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index e5ec23178d..198fd495bd 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -188,9 +188,7 @@ struct UserProfile: View { } func profileImageView(_ imageStr: String?) -> some View { - ProfileImage(imageStr: imageStr) - .aspectRatio(1, contentMode: .fit) - .frame(maxWidth: 192, maxHeight: 192) + ProfileImage(imageStr: imageStr, size: 192) } func editImageButton(action: @escaping () -> Void) -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index af3d60c9c0..8c1a3bf4e1 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -309,8 +309,7 @@ struct UserProfilesView: View { } } label: { HStack { - ProfileImage(imageStr: user.image, color: Color(uiColor: .tertiarySystemFill)) - .frame(width: 44, height: 44) + ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill)) .padding(.vertical, 4) .padding(.trailing, 12) Text(user.chatViewName) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 0f8bff18ea..3d0bb2bf2c 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -109,6 +109,7 @@ %@ downloaded + %@ изтеглено No comment provided by engineer. @@ -133,6 +134,7 @@ %@ uploaded + %@ качено No comment provided by engineer. @@ -352,6 +354,7 @@ **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **Моля, обърнете внимание**: използването на една и съща база данни на две устройства ще наруши декриптирането на съобщенията от вашите връзки като защита на сигурността. No comment provided by engineer. @@ -371,6 +374,7 @@ **Warning**: the archive will be removed. + **Внимание**: архивът ще бъде изтрит. No comment provided by engineer. @@ -739,6 +743,11 @@ Позволи необратимо изтриване на изпратените съобщения. (24 часа) No comment provided by engineer. + + Allow to send SimpleX links. + Разрешаване на изпращане на SimpleX линкове. + No comment provided by engineer. + Allow to send files and media. Позволи изпращане на файлове и медия. @@ -1079,6 +1088,11 @@ Файлът не може да бъде получен No comment provided by engineer. + + Cellular + Мобилна мрежа + No comment provided by engineer. + Change Промени @@ -1197,6 +1211,7 @@ Choose _Migrate from another device_ on the new device and scan QR code. + Изберете _Мигриране от друго устройство_ на новото устройство и сканирайте QR кода. No comment provided by engineer. @@ -2073,6 +2088,11 @@ This cannot be undone! Понижи версията и отвори чата No comment provided by engineer. + + Download + Изтегли + chat item action + Download failed Неуспешно изтегляне @@ -2183,6 +2203,11 @@ This cannot be undone! Активирай kод за достъп за самоунищожение set passcode view + + Enabled for + Активирано за + No comment provided by engineer. + Encrypt Криптирай @@ -2333,11 +2358,6 @@ This cannot be undone! Грешка при добавяне на член(ове) No comment provided by engineer. - - Error allowing contact PQ encryption - Грешка при разрешаване на PQ криптиране за контакт - No comment provided by engineer. - Error changing address Грешка при промяна на адреса @@ -2708,6 +2728,11 @@ This cannot be undone! Файловете и медията са забранени в тази група. No comment provided by engineer. + + Files and media not allowed + Файлове и медия не са разрешени + No comment provided by engineer. + Files and media prohibited! Файловете и медията са забранени! @@ -2773,6 +2798,26 @@ This cannot be undone! За конзолата No comment provided by engineer. + + Forward + Препрати + chat item action + + + Forward and save messages + Препращане и запазване на съобщения + No comment provided by engineer. + + + Forwarded + Препратено + No comment provided by engineer. + + + Forwarded from + Препратено от + No comment provided by engineer. + Found desktop Намерено настолно устройство @@ -2883,6 +2928,11 @@ This cannot be undone! Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) No comment provided by engineer. + + Group members can send SimpleX links. + Членовете на групата могат да изпращат SimpleX линкове. + No comment provided by engineer. + Group members can send direct messages. Членовете на групата могат да изпращат лични съобщения. @@ -3123,6 +3173,11 @@ This cannot be undone! В отговор на No comment provided by engineer. + + In-call sounds + Звуци по време на разговор + No comment provided by engineer. + Incognito Инкогнито @@ -3598,6 +3653,11 @@ This is your link for group %@! Реакциите на съобщения са забранени в тази група. No comment provided by engineer. + + Message source remains private. + Източникът на съобщението остава скрит. + No comment provided by engineer. + Message text Текст на съобщението @@ -3625,10 +3685,12 @@ This is your link for group %@! Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. + Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом. No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. + Съобщенията, файловете и разговорите са защитени чрез **квантово устойчиво e2e криптиране** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом. No comment provided by engineer. @@ -3711,6 +3773,11 @@ This is your link for group %@! Очаквайте скоро още подобрения! No comment provided by engineer. + + More reliable network connection. + По-надеждна мрежова връзка. + No comment provided by engineer. + Most likely this connection is deleted. Най-вероятно тази връзка е изтрита. @@ -3746,6 +3813,16 @@ This is your link for group %@! Мрежа и сървъри No comment provided by engineer. + + Network connection + Мрежова връзка + No comment provided by engineer. + + + Network management + Управление на мрежата + No comment provided by engineer. + Network settings Мрежови настройки @@ -3856,6 +3933,11 @@ This is your link for group %@! Няма история No comment provided by engineer. + + No network connection + Няма мрежова връзка + No comment provided by engineer. + No permission to record voice message Няма разрешение за запис на гласово съобщение @@ -4070,6 +4152,11 @@ This is your link for group %@! Или покажи този код No comment provided by engineer. + + Other + Други + No comment provided by engineer. + PING count PING бройка @@ -4237,11 +4324,6 @@ Error: %@ Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен server test error - - Post-quantum E2EE - Постквантово E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Запазете последната чернова на съобщението с прикачени файлове. @@ -4292,6 +4374,11 @@ Error: %@ Профилно изображение No comment provided by engineer. + + Profile images + Профилни изображения + No comment provided by engineer. + Profile name Име на профила @@ -4332,6 +4419,11 @@ Error: %@ Забрани реакциите на съобщенията. No comment provided by engineer. + + Prohibit sending SimpleX links. + Забранете изпращането на SimpleX линкове. + No comment provided by engineer. + Prohibit sending direct messages to members. Забрани изпращането на лични съобщения до членовете. @@ -4462,6 +4554,11 @@ Error: %@ Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн. No comment provided by engineer. + + Receiving concurrency + Паралелност на получаване + No comment provided by engineer. + Receiving file will be stopped. Получаващият се файл ще бъде спрян. @@ -4477,6 +4574,11 @@ Error: %@ Скорошна история и подобрен [bot за директория за групи](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPd jdLW3%23%2F%3Fv%3D1-2% 26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Получателят(ите) не могат да видят от кого е това съобщение. + No comment provided by engineer. + Recipients see updates as you type them. Получателите виждат актуализации, докато ги въвеждате. @@ -4777,11 +4879,21 @@ Error: %@ Запази съобщението при посрещане? No comment provided by engineer. + + Saved + Запазено + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Запазените WebRTC ICE сървъри ще бъдат премахнати No comment provided by engineer. + + Saved from + Запазено от + No comment provided by engineer. + Saved message Запазено съобщение @@ -5087,6 +5199,11 @@ Error: %@ Настройки No comment provided by engineer. + + Shape profile images + Променете формата на профилните изображения + No comment provided by engineer. + Share Сподели @@ -5205,6 +5322,16 @@ Error: %@ SimpleX links SimpleX линкове + chat feature + + + SimpleX links are prohibited in this group. + SimpleX линкове са забранени в тази група. + No comment provided by engineer. + + + SimpleX links not allowed + SimpleX линковете не са разрешени No comment provided by engineer. @@ -5242,6 +5369,11 @@ Error: %@ Някой notification title + + Square, circle, or anything in between. + Квадрат, кръг или нещо между тях. + No comment provided by engineer. + Start chat Започни чат @@ -6043,6 +6175,11 @@ To connect, please ask your contact to create another connection link and check Гласовите съобщения са забранени в тази група. No comment provided by engineer. + + Voice messages not allowed + Гласовите съобщения не са разрешени + No comment provided by engineer. + Voice messages prohibited! Гласовите съобщения са забранени! @@ -6113,6 +6250,11 @@ To connect, please ask your contact to create another connection link and check Когато са налични No comment provided by engineer. + + When connecting audio and video calls. + При свързване на аудио и видео разговори. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите. @@ -6123,6 +6265,21 @@ To connect, please ask your contact to create another connection link and check Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Ще бъде активирано в личните чатове! + No comment provided by engineer. + + + Wired ethernet + Кабелен Ethernet + No comment provided by engineer. + With encrypted files and media. С криптирани файлове и медия. @@ -6160,6 +6317,7 @@ To connect, please ask your contact to create another connection link and check You **must not** use the same database on two devices. + **Не трябва** да използвате една и съща база данни на две устройства. No comment provided by engineer. @@ -6605,6 +6763,11 @@ SimpleX сървърите не могат да видят вашия профи админ member role + + admins + администратори + feature role + agreeing encryption for %@… съгласуване на криптиране за %@… @@ -6615,6 +6778,11 @@ SimpleX сървърите не могат да видят вашия профи съгласуване на криптиране… chat item text + + all members + всички членове + feature role + always винаги @@ -6945,6 +7113,11 @@ SimpleX сървърите не могат да видят вашия профи събитие се случи No comment provided by engineer. + + forwarded + препратено + No comment provided by engineer. + group deleted групата е изтрита @@ -7152,6 +7325,11 @@ SimpleX сървърите не могат да видят вашия профи собственик member role + + owners + собственици + feature role + peer-to-peer peer-to-peer @@ -7202,6 +7380,16 @@ SimpleX сървърите не могат да видят вашия профи ви острани rcv group event chat item + + saved + запазено + No comment provided by engineer. + + + saved from %@ + запазено от %@ + No comment provided by engineer. + sec сек. @@ -7342,6 +7530,11 @@ SimpleX сървърите не могат да видят вашия профи да pref value + + you + вие + No comment provided by engineer. + you are invited to group вие сте поканени в групата diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index cf6e0e1fcd..a1ef39b2fb 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -720,6 +720,10 @@ Povolit nevratné smazání odeslaných zpráv. (24 hodin) No comment provided by engineer. + + Allow to send SimpleX links. + No comment provided by engineer. + Allow to send files and media. Povolit odesílání souborů a médii. @@ -1043,6 +1047,10 @@ Nelze přijmout soubor No comment provided by engineer. + + Cellular + No comment provided by engineer. + Change Změnit @@ -1999,6 +2007,10 @@ This cannot be undone! Snížit a otevřít chat No comment provided by engineer. + + Download + chat item action + Download failed No comment provided by engineer. @@ -2104,6 +2116,10 @@ This cannot be undone! Povolit sebedestrukční heslo set passcode view + + Enabled for + No comment provided by engineer. + Encrypt Šifrovat @@ -2247,10 +2263,6 @@ This cannot be undone! Chyba přidávání člena(ů) No comment provided by engineer. - - Error allowing contact PQ encryption - No comment provided by engineer. - Error changing address Chuba změny adresy @@ -2611,6 +2623,10 @@ This cannot be undone! Soubory a média jsou zakázány v této skupině. No comment provided by engineer. + + Files and media not allowed + No comment provided by engineer. + Files and media prohibited! Soubory a média jsou zakázány! @@ -2674,6 +2690,22 @@ This cannot be undone! Pro konzoli No comment provided by engineer. + + Forward + chat item action + + + Forward and save messages + No comment provided by engineer. + + + Forwarded + No comment provided by engineer. + + + Forwarded from + No comment provided by engineer. + Found desktop No comment provided by engineer. @@ -2780,6 +2812,10 @@ This cannot be undone! Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin) No comment provided by engineer. + + Group members can send SimpleX links. + No comment provided by engineer. + Group members can send direct messages. Členové skupiny mohou posílat přímé zprávy. @@ -3014,6 +3050,10 @@ This cannot be undone! V odpovědi na No comment provided by engineer. + + In-call sounds + No comment provided by engineer. + Incognito Inkognito @@ -3470,6 +3510,10 @@ This is your link for group %@! Reakce na zprávy jsou v této skupině zakázány. No comment provided by engineer. + + Message source remains private. + No comment provided by engineer. + Message text Text zprávy @@ -3574,6 +3618,10 @@ This is your link for group %@! Další vylepšení se chystají již brzy! No comment provided by engineer. + + More reliable network connection. + No comment provided by engineer. + Most likely this connection is deleted. Pravděpodobně je toto spojení smazáno. @@ -3609,6 +3657,14 @@ This is your link for group %@! Síť a servery No comment provided by engineer. + + Network connection + No comment provided by engineer. + + + Network management + No comment provided by engineer. + Network settings Nastavení sítě @@ -3718,6 +3774,10 @@ This is your link for group %@! Žádná historie No comment provided by engineer. + + No network connection + No comment provided by engineer. + No permission to record voice message Nemáte oprávnění nahrávat hlasové zprávy @@ -3923,6 +3983,10 @@ This is your link for group %@! Or show this code No comment provided by engineer. + + Other + No comment provided by engineer. + PING count Počet PING @@ -4082,10 +4146,6 @@ Error: %@ Je možné, že otisk certifikátu v adrese serveru je nesprávný server test error - - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Zachování posledního návrhu zprávy s přílohami. @@ -4135,6 +4195,10 @@ Error: %@ Profilový obrázek No comment provided by engineer. + + Profile images + No comment provided by engineer. + Profile name No comment provided by engineer. @@ -4173,6 +4237,10 @@ Error: %@ Zakázat reakce na zprávy. No comment provided by engineer. + + Prohibit sending SimpleX links. + No comment provided by engineer. + Prohibit sending direct messages to members. Zakázat odesílání přímých zpráv členům. @@ -4300,6 +4368,10 @@ Error: %@ Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele. No comment provided by engineer. + + Receiving concurrency + No comment provided by engineer. + Receiving file will be stopped. Příjem souboru bude zastaven. @@ -4314,6 +4386,10 @@ Error: %@ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + No comment provided by engineer. + Recipients see updates as you type them. Příjemci uvidí aktualizace během jejich psaní. @@ -4607,11 +4683,19 @@ Error: %@ Uložit uvítací zprávu? No comment provided by engineer. + + Saved + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Uložené servery WebRTC ICE budou odstraněny No comment provided by engineer. + + Saved from + No comment provided by engineer. + Saved message message info title @@ -4910,6 +4994,10 @@ Error: %@ Nastavení No comment provided by engineer. + + Shape profile images + No comment provided by engineer. + Share Sdílet @@ -5026,6 +5114,14 @@ Error: %@ SimpleX links Odkazy na SimpleX + chat feature + + + SimpleX links are prohibited in this group. + No comment provided by engineer. + + + SimpleX links not allowed No comment provided by engineer. @@ -5063,6 +5159,10 @@ Error: %@ Někdo notification title + + Square, circle, or anything in between. + No comment provided by engineer. + Start chat Začít chat @@ -5828,6 +5928,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Hlasové zprávy jsou v této skupině zakázány. No comment provided by engineer. + + Voice messages not allowed + No comment provided by engineer. + Voice messages prohibited! Hlasové zprávy jsou zakázány! @@ -5895,6 +5999,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Když je k dispozici No comment provided by engineer. + + When connecting audio and video calls. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout. @@ -5905,6 +6013,18 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve. No comment provided by engineer. + + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + No comment provided by engineer. + + + Wired ethernet + No comment provided by engineer. + With encrypted files and media. No comment provided by engineer. @@ -6368,6 +6488,10 @@ Servery SimpleX nevidí váš profil. správce member role + + admins + feature role + agreeing encryption for %@… povoluji šifrování pro %@… @@ -6378,6 +6502,10 @@ Servery SimpleX nevidí váš profil. povoluji šifrování… chat item text + + all members + feature role + always vždy @@ -6700,6 +6828,10 @@ Servery SimpleX nevidí váš profil. event happened No comment provided by engineer. + + forwarded + No comment provided by engineer. + group deleted skupina smazána @@ -6906,6 +7038,10 @@ Servery SimpleX nevidí váš profil. vlastník member role + + owners + feature role + peer-to-peer peer-to-peer @@ -6953,6 +7089,14 @@ Servery SimpleX nevidí váš profil. odstranil vás rcv group event chat item + + saved + No comment provided by engineer. + + + saved from %@ + No comment provided by engineer. + sec sek @@ -7086,6 +7230,10 @@ Servery SimpleX nevidí váš profil. ano pref value + + you + No comment provided by engineer. + you are invited to group jste pozváni do skupiny diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 7ad19ff86f..29384aecc4 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -743,6 +743,11 @@ Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden) No comment provided by engineer. + + Allow to send SimpleX links. + Das Senden von SimpleX-Links erlauben. + No comment provided by engineer. + Allow to send files and media. Das Senden von Dateien und Medien erlauben. @@ -1083,6 +1088,11 @@ Datei kann nicht empfangen werden No comment provided by engineer. + + Cellular + Zellulär + No comment provided by engineer. + Change Ändern @@ -2078,6 +2088,11 @@ Das kann nicht rückgängig gemacht werden! Datenbank herabstufen und den Chat öffnen No comment provided by engineer. + + Download + Herunterladen + chat item action + Download failed Herunterladen fehlgeschlagen @@ -2188,6 +2203,11 @@ Das kann nicht rückgängig gemacht werden! Selbstzerstörungs-Zugangscode aktivieren set passcode view + + Enabled for + Aktiviert für + No comment provided by engineer. + Encrypt Verschlüsseln @@ -2338,11 +2358,6 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Hinzufügen von Mitgliedern No comment provided by engineer. - - Error allowing contact PQ encryption - Fehler beim Zulassen der Kontakt-PQ-Verschlüsselung - No comment provided by engineer. - Error changing address Fehler beim Wechseln der Empfängeradresse @@ -2713,6 +2728,11 @@ Das kann nicht rückgängig gemacht werden! In dieser Gruppe sind Dateien und Medien nicht erlaubt. No comment provided by engineer. + + Files and media not allowed + Dateien und Medien sind nicht erlaubt + No comment provided by engineer. + Files and media prohibited! Dateien und Medien sind nicht erlaubt! @@ -2778,6 +2798,26 @@ Das kann nicht rückgängig gemacht werden! Für Konsole No comment provided by engineer. + + Forward + Weiterleiten + chat item action + + + Forward and save messages + Nachrichten weiterleiten und speichern + No comment provided by engineer. + + + Forwarded + Weitergeleitet + No comment provided by engineer. + + + Forwarded from + Weitergeleitet aus + No comment provided by engineer. + Found desktop Gefundener Desktop @@ -2888,6 +2928,11 @@ Das kann nicht rückgängig gemacht werden! Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) No comment provided by engineer. + + Group members can send SimpleX links. + Gruppenmitglieder können SimpleX-Links senden. + No comment provided by engineer. + Group members can send direct messages. Gruppenmitglieder können Direktnachrichten versenden. @@ -3128,6 +3173,11 @@ Das kann nicht rückgängig gemacht werden! Als Antwort auf No comment provided by engineer. + + In-call sounds + Klingeltöne + No comment provided by engineer. + Incognito Inkognito @@ -3603,6 +3653,11 @@ Das ist Ihr Link für die Gruppe %@! In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt. No comment provided by engineer. + + Message source remains private. + Die Nachrichtenquelle bleibt privat. + No comment provided by engineer. + Message text Nachrichtentext @@ -3718,6 +3773,11 @@ Das ist Ihr Link für die Gruppe %@! Weitere Verbesserungen sind bald verfügbar! No comment provided by engineer. + + More reliable network connection. + Zuverlässigere Netzwerkverbindung. + No comment provided by engineer. + Most likely this connection is deleted. Wahrscheinlich ist diese Verbindung gelöscht worden. @@ -3753,6 +3813,16 @@ Das ist Ihr Link für die Gruppe %@! Netzwerk & Server No comment provided by engineer. + + Network connection + Netzwerkverbindung + No comment provided by engineer. + + + Network management + Netzwerk-Verwaltung + No comment provided by engineer. + Network settings Netzwerkeinstellungen @@ -3863,6 +3933,11 @@ Das ist Ihr Link für die Gruppe %@! Kein Nachrichtenverlauf No comment provided by engineer. + + No network connection + Keine Netzwerkverbindung + No comment provided by engineer. + No permission to record voice message Keine Berechtigung für das Aufnehmen von Sprachnachrichten @@ -4077,6 +4152,11 @@ Das ist Ihr Link für die Gruppe %@! Oder diesen QR-Code anzeigen No comment provided by engineer. + + Other + Andere + No comment provided by engineer. + PING count PING-Zähler @@ -4244,11 +4324,6 @@ Fehler: %@ Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig server test error - - Post-quantum E2EE - Post-Quantum E2E-Verschlüsselung - No comment provided by engineer. - Preserve the last message draft, with attachments. Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren. @@ -4299,6 +4374,11 @@ Fehler: %@ Profilbild No comment provided by engineer. + + Profile images + Profil-Bilder + No comment provided by engineer. + Profile name Profilname @@ -4339,6 +4419,11 @@ Fehler: %@ Reaktionen auf Nachrichten nicht erlauben. No comment provided by engineer. + + Prohibit sending SimpleX links. + Das Senden von SimpleX-Links nicht erlauben. + No comment provided by engineer. + Prohibit sending direct messages to members. Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben. @@ -4346,7 +4431,7 @@ Fehler: %@ Prohibit sending disappearing messages. - Das Senden von verschwindenden Nachrichten verbieten. + Das Senden von verschwindenden Nachrichten nicht erlauben. No comment provided by engineer. @@ -4469,6 +4554,11 @@ Fehler: %@ Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist. No comment provided by engineer. + + Receiving concurrency + Gleichzeitiger Empfang + No comment provided by engineer. + Receiving file will be stopped. Der Empfang der Datei wird beendet. @@ -4484,6 +4574,11 @@ Fehler: %@ Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Empfänger können nicht sehen, von wem die Nachricht stammt. + No comment provided by engineer. + Recipients see updates as you type them. Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben. @@ -4784,11 +4879,21 @@ Fehler: %@ Begrüßungsmeldung speichern? No comment provided by engineer. + + Saved + Abgespeichert + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Gespeicherte WebRTC ICE-Server werden entfernt No comment provided by engineer. + + Saved from + Abgespeichert von + No comment provided by engineer. + Saved message Gespeicherte Nachricht @@ -5094,6 +5199,11 @@ Fehler: %@ Einstellungen No comment provided by engineer. + + Shape profile images + Form der Profil-Bilder + No comment provided by engineer. + Share Teilen @@ -5212,6 +5322,16 @@ Fehler: %@ SimpleX links SimpleX-Links + chat feature + + + SimpleX links are prohibited in this group. + In dieser Gruppe sind SimpleX-Links nicht erlaubt. + No comment provided by engineer. + + + SimpleX links not allowed + SimpleX-Links sind nicht erlaubt No comment provided by engineer. @@ -5249,6 +5369,11 @@ Fehler: %@ Jemand notification title + + Square, circle, or anything in between. + Quadratisch, kreisförmig oder irgendetwas dazwischen. + No comment provided by engineer. + Start chat Starten Sie den Chat @@ -5907,7 +6032,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use current profile - Nutzen Sie das aktuelle Profil + Das aktuelle Profil nutzen No comment provided by engineer. @@ -5927,7 +6052,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use new incognito profile - Nutzen Sie das neue Inkognito-Profil + Ein neues Inkognito-Profil nutzen No comment provided by engineer. @@ -6050,6 +6175,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s In dieser Gruppe sind Sprachnachrichten nicht erlaubt. No comment provided by engineer. + + Voice messages not allowed + Sprachnachrichten sind nicht erlaubt + No comment provided by engineer. + Voice messages prohibited! Sprachnachrichten sind nicht erlaubt! @@ -6120,6 +6250,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Wenn verfügbar No comment provided by engineer. + + When connecting audio and video calls. + Bei der Verbindung über Audio- und Video-Anrufe. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen. @@ -6130,6 +6265,21 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Wird in direkten Chats automatisch aktiviert! + No comment provided by engineer. + + + Wired ethernet + Kabelgebundenes Netzwerk + No comment provided by engineer. + With encrypted files and media. Mit verschlüsselten Dateien und Medien. @@ -6613,6 +6763,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Admin member role + + admins + Administratoren + feature role + agreeing encryption for %@… Verschlüsselung von %@ zustimmen… @@ -6623,6 +6778,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Verschlüsselung zustimmen… chat item text + + all members + Alle Mitglieder + feature role + always Immer @@ -6953,6 +7113,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. event happened No comment provided by engineer. + + forwarded + weitergeleitet + No comment provided by engineer. + group deleted Gruppe gelöscht @@ -7160,6 +7325,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Eigentümer member role + + owners + Eigentümer + feature role + peer-to-peer Peer-to-Peer @@ -7210,6 +7380,16 @@ SimpleX-Server können Ihr Profil nicht einsehen. hat Sie aus der Gruppe entfernt rcv group event chat item + + saved + abgespeichert + No comment provided by engineer. + + + saved from %@ + abgespeichert von %@ + No comment provided by engineer. + sec sek @@ -7350,6 +7530,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Ja pref value + + you + Profil + No comment provided by engineer. + you are invited to group Sie sind zu der Gruppe eingeladen diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 39ee4e2a9b..2b5677dd61 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -743,6 +743,11 @@ Allow to irreversibly delete sent messages. (24 hours) No comment provided by engineer. + + Allow to send SimpleX links. + Allow to send SimpleX links. + No comment provided by engineer. + Allow to send files and media. Allow to send files and media. @@ -1083,6 +1088,11 @@ Cannot receive file No comment provided by engineer. + + Cellular + Cellular + No comment provided by engineer. + Change Change @@ -2078,6 +2088,11 @@ This cannot be undone! Downgrade and open chat No comment provided by engineer. + + Download + Download + chat item action + Download failed Download failed @@ -2188,6 +2203,11 @@ This cannot be undone! Enable self-destruct passcode set passcode view + + Enabled for + Enabled for + No comment provided by engineer. + Encrypt Encrypt @@ -2338,11 +2358,6 @@ This cannot be undone! Error adding member(s) No comment provided by engineer. - - Error allowing contact PQ encryption - Error allowing contact PQ encryption - No comment provided by engineer. - Error changing address Error changing address @@ -2713,6 +2728,11 @@ This cannot be undone! Files and media are prohibited in this group. No comment provided by engineer. + + Files and media not allowed + Files and media not allowed + No comment provided by engineer. + Files and media prohibited! Files and media prohibited! @@ -2778,6 +2798,26 @@ This cannot be undone! For console No comment provided by engineer. + + Forward + Forward + chat item action + + + Forward and save messages + Forward and save messages + No comment provided by engineer. + + + Forwarded + Forwarded + No comment provided by engineer. + + + Forwarded from + Forwarded from + No comment provided by engineer. + Found desktop Found desktop @@ -2888,6 +2928,11 @@ This cannot be undone! Group members can irreversibly delete sent messages. (24 hours) No comment provided by engineer. + + Group members can send SimpleX links. + Group members can send SimpleX links. + No comment provided by engineer. + Group members can send direct messages. Group members can send direct messages. @@ -3128,6 +3173,11 @@ This cannot be undone! In reply to No comment provided by engineer. + + In-call sounds + In-call sounds + No comment provided by engineer. + Incognito Incognito @@ -3603,6 +3653,11 @@ This is your link for group %@! Message reactions are prohibited in this group. No comment provided by engineer. + + Message source remains private. + Message source remains private. + No comment provided by engineer. + Message text Message text @@ -3718,6 +3773,11 @@ This is your link for group %@! More improvements are coming soon! No comment provided by engineer. + + More reliable network connection. + More reliable network connection. + No comment provided by engineer. + Most likely this connection is deleted. Most likely this connection is deleted. @@ -3753,6 +3813,16 @@ This is your link for group %@! Network & servers No comment provided by engineer. + + Network connection + Network connection + No comment provided by engineer. + + + Network management + Network management + No comment provided by engineer. + Network settings Network settings @@ -3863,6 +3933,11 @@ This is your link for group %@! No history No comment provided by engineer. + + No network connection + No network connection + No comment provided by engineer. + No permission to record voice message No permission to record voice message @@ -4077,6 +4152,11 @@ This is your link for group %@! Or show this code No comment provided by engineer. + + Other + Other + No comment provided by engineer. + PING count PING count @@ -4244,11 +4324,6 @@ Error: %@ Possibly, certificate fingerprint in server address is incorrect server test error - - Post-quantum E2EE - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Preserve the last message draft, with attachments. @@ -4299,6 +4374,11 @@ Error: %@ Profile image No comment provided by engineer. + + Profile images + Profile images + No comment provided by engineer. + Profile name Profile name @@ -4339,6 +4419,11 @@ Error: %@ Prohibit messages reactions. No comment provided by engineer. + + Prohibit sending SimpleX links. + Prohibit sending SimpleX links. + No comment provided by engineer. + Prohibit sending direct messages to members. Prohibit sending direct messages to members. @@ -4469,6 +4554,11 @@ Error: %@ Receiving address will be changed to a different server. Address change will complete after sender comes online. No comment provided by engineer. + + Receiving concurrency + Receiving concurrency + No comment provided by engineer. + Receiving file will be stopped. Receiving file will be stopped. @@ -4484,6 +4574,11 @@ Error: %@ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Recipient(s) can't see who this message is from. + No comment provided by engineer. + Recipients see updates as you type them. Recipients see updates as you type them. @@ -4784,11 +4879,21 @@ Error: %@ Save welcome message? No comment provided by engineer. + + Saved + Saved + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Saved WebRTC ICE servers will be removed No comment provided by engineer. + + Saved from + Saved from + No comment provided by engineer. + Saved message Saved message @@ -5094,6 +5199,11 @@ Error: %@ Settings No comment provided by engineer. + + Shape profile images + Shape profile images + No comment provided by engineer. + Share Share @@ -5212,6 +5322,16 @@ Error: %@ SimpleX links SimpleX links + chat feature + + + SimpleX links are prohibited in this group. + SimpleX links are prohibited in this group. + No comment provided by engineer. + + + SimpleX links not allowed + SimpleX links not allowed No comment provided by engineer. @@ -5249,6 +5369,11 @@ Error: %@ Somebody notification title + + Square, circle, or anything in between. + Square, circle, or anything in between. + No comment provided by engineer. + Start chat Start chat @@ -6050,6 +6175,11 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this group. No comment provided by engineer. + + Voice messages not allowed + Voice messages not allowed + No comment provided by engineer. + Voice messages prohibited! Voice messages prohibited! @@ -6120,6 +6250,11 @@ To connect, please ask your contact to create another connection link and check When available No comment provided by engineer. + + When connecting audio and video calls. + When connecting audio and video calls. + No comment provided by engineer. + When people request to connect, you can accept or reject it. When people request to connect, you can accept or reject it. @@ -6130,6 +6265,21 @@ To connect, please ask your contact to create another connection link and check When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Will be enabled in direct chats! + No comment provided by engineer. + + + Wired ethernet + Wired ethernet + No comment provided by engineer. + With encrypted files and media. With encrypted files and media. @@ -6613,6 +6763,11 @@ SimpleX servers cannot see your profile. admin member role + + admins + admins + feature role + agreeing encryption for %@… agreeing encryption for %@… @@ -6623,6 +6778,11 @@ SimpleX servers cannot see your profile. agreeing encryption… chat item text + + all members + all members + feature role + always always @@ -6953,6 +7113,11 @@ SimpleX servers cannot see your profile. event happened No comment provided by engineer. + + forwarded + forwarded + No comment provided by engineer. + group deleted group deleted @@ -7160,6 +7325,11 @@ SimpleX servers cannot see your profile. owner member role + + owners + owners + feature role + peer-to-peer peer-to-peer @@ -7210,6 +7380,16 @@ SimpleX servers cannot see your profile. removed you rcv group event chat item + + saved + saved + No comment provided by engineer. + + + saved from %@ + saved from %@ + No comment provided by engineer. + sec sec @@ -7350,6 +7530,11 @@ SimpleX servers cannot see your profile. yes pref value + + you + you + No comment provided by engineer. + you are invited to group you are invited to group diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 99a117d786..5d2482ae27 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -109,7 +109,7 @@ %@ downloaded - %@ downloaded + %@ descargado No comment provided by engineer. @@ -134,7 +134,7 @@ %@ uploaded - %@ cargado + %@ subido No comment provided by engineer. @@ -354,7 +354,7 @@ **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. - **Tenga en cuenta**: usar la misma base de datos en dos dispositivos interrumpirá el descifrado de mensajes de sus conexiones, como protección de seguridad. + **Recuarda**: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad. No comment provided by engineer. @@ -635,7 +635,7 @@ Admins can block a member for all. - Los admins pueden bloquear un miembro por todos. + Los administradores pueden bloquear a un miembro para los demás. No comment provided by engineer. @@ -695,7 +695,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Todos sus contactos, conversaciones y archivos se cifrarán de forma segura y se subirán en fragmentos hacia puntos XFTP configurados. + Todos tus contactos, conversaciones y archivos serán cifrados, divididos y subidos de forma segura a los servidores XFTP configurados. No comment provided by engineer. @@ -743,6 +743,11 @@ Se permite la eliminación irreversible de mensajes. (24 horas) No comment provided by engineer. + + Allow to send SimpleX links. + Permitir enviar enlaces SimpleX. + No comment provided by engineer. + Allow to send files and media. Se permite enviar archivos y multimedia. @@ -825,7 +830,7 @@ App data migration - Migración de datos de la aplicación + Migrar datos de la aplicación No comment provided by engineer. @@ -870,7 +875,7 @@ Archive and upload - Archivar y transferir + Archivar y subir No comment provided by engineer. @@ -1000,7 +1005,7 @@ Blocked by admin - Bloqueado por el administrador + Bloqueado por administrador No comment provided by engineer. @@ -1083,6 +1088,11 @@ No se puede recibir el archivo No comment provided by engineer. + + Cellular + Móvil + No comment provided by engineer. + Change Cambiar @@ -1166,17 +1176,17 @@ Chat is stopped - Chat está detenido + Chat está parado No comment provided by engineer. Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. - Chat está detenido. Si estás usando esta base de datos en otro dispositivo, deberías transferirla de vuelta antes de iniciarlo. + Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar Chat. No comment provided by engineer. Chat migrated! - Chat transferido ! + ¡Chat migrado! No comment provided by engineer. @@ -1201,7 +1211,7 @@ Choose _Migrate from another device_ on the new device and scan QR code. - Elija _Migrar desde otro dispositivo_ en el nuevo dispositivo y escanee el código QR. + En el nuevo dispositivo selecciona _Migrar desde otro dispositivo_ y escanéa el código QR. No comment provided by engineer. @@ -1276,7 +1286,7 @@ Confirm network settings - Confirmar los ajustes de red + Confirmar configuración de red No comment provided by engineer. @@ -1291,7 +1301,7 @@ Confirm that you remember database passphrase to migrate it. - Confirme que recuerda la frase secreta de la base de datos para migrarla. + Confirma que recuerdas la frase de contraseña de la base de datos para migrarla. No comment provided by engineer. @@ -1560,7 +1570,7 @@ This is your own one-time link! Creating archive link - Creando enlace de archivo + Creando enlace al archivo No comment provided by engineer. @@ -1758,7 +1768,7 @@ This is your own one-time link! Delete chat profile? - ¿Eliminar el perfil? + ¿Eliminar perfil? No comment provided by engineer. @@ -2078,9 +2088,14 @@ This cannot be undone! Degradar y abrir Chat No comment provided by engineer. + + Download + Descargar + chat item action + Download failed - Error en la descarga + Descarga fallida No comment provided by engineer. @@ -2155,7 +2170,7 @@ This cannot be undone! Enable in direct chats (BETA)! - Activar en chats directos (BETA)! + ¡Activar en chats directos (BETA)! No comment provided by engineer. @@ -2188,6 +2203,11 @@ This cannot be undone! Activar código de autodestrucción set passcode view + + Enabled for + Activar para + No comment provided by engineer. + Encrypt Cifrar @@ -2275,7 +2295,7 @@ This cannot be undone! Enter passphrase - Introducir frase de contraseña + Introduce la frase de contraseña No comment provided by engineer. @@ -2338,11 +2358,6 @@ This cannot be undone! Error al añadir miembro(s) No comment provided by engineer. - - Error allowing contact PQ encryption - Error al permitir cifrado PQ al contacto - No comment provided by engineer. - Error changing address Error al cambiar servidor @@ -2555,7 +2570,7 @@ This cannot be undone! Error stopping chat - Error al detener Chat + Error al parar Chat No comment provided by engineer. @@ -2685,12 +2700,12 @@ This cannot be undone! File will be received when your contact completes uploading it. - El archivo se recibirá cuando tu contacto termine de subirlo. + El archivo se recibirá cuando el contacto termine de subirlo. No comment provided by engineer. File will be received when your contact is online, please wait or check later! - El archivo se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde! + El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde! No comment provided by engineer. @@ -2710,7 +2725,12 @@ This cannot be undone! Files and media are prohibited in this group. - No se permiten archivos y multimedia en este grupo. + Los archivos y multimedia no están permitidos en este grupo. + No comment provided by engineer. + + + Files and media not allowed + Archivos y multimedia no permitidos No comment provided by engineer. @@ -2725,7 +2745,7 @@ This cannot be undone! Finalize migration - Finalizar la migración + Finalizar migración No comment provided by engineer. @@ -2778,6 +2798,26 @@ This cannot be undone! Para consola No comment provided by engineer. + + Forward + Reenviar + chat item action + + + Forward and save messages + Reenviar y guardar mensajes + No comment provided by engineer. + + + Forwarded + Reenviado + No comment provided by engineer. + + + Forwarded from + Reenviado por + No comment provided by engineer. + Found desktop Ordenador encontrado @@ -2888,6 +2928,11 @@ This cannot be undone! Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) No comment provided by engineer. + + Group members can send SimpleX links. + Los miembros del grupo pueden enviar enlaces SimpleX. + No comment provided by engineer. + Group members can send direct messages. Los miembros del grupo pueden enviar mensajes directos. @@ -3025,7 +3070,7 @@ This cannot be undone! Hungarian interface - Interfaz húngara + Interfaz en húngaro No comment provided by engineer. @@ -3060,12 +3105,12 @@ This cannot be undone! Image will be received when your contact completes uploading it. - La imagen se recibirá cuando tu contacto termine de subirla. + La imagen se recibirá cuando el contacto termine de subirla. No comment provided by engineer. Image will be received when your contact is online, please wait or check later! - La imagen se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde! + La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde! No comment provided by engineer. @@ -3120,7 +3165,7 @@ This cannot be undone! In order to continue, chat should be stopped. - Para continuar, el chat debe ser interrumpido. + Para continuar, Chat debe estar parado. No comment provided by engineer. @@ -3128,6 +3173,11 @@ This cannot be undone! En respuesta a No comment provided by engineer. + + In-call sounds + Sonido de llamada + No comment provided by engineer. + Incognito Incógnito @@ -3237,7 +3287,7 @@ This cannot be undone! Invalid migration confirmation - Confirmación de migración inválida + Confirmación de migración no válida No comment provided by engineer. @@ -3311,7 +3361,7 @@ This cannot be undone! 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised. Esto puede suceder cuando: -1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días. +1. Los mensajes caducan tras 2 días en el cliente saliente o tras 30 días en el servidor. 2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos. 3. La conexión ha sido comprometida. No comment provided by engineer. @@ -3603,6 +3653,11 @@ This is your link for group %@! Las reacciones a los mensajes no están permitidas en este grupo. No comment provided by engineer. + + Message source remains private. + El autor del mensaje se mantiene privado. + No comment provided by engineer. + Message text Contacto y texto @@ -3610,7 +3665,7 @@ This is your link for group %@! Message too large - Mensaje demasiado grande + Mensaje demasiado largo No comment provided by engineer. @@ -3630,12 +3685,12 @@ This is your link for group %@! Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. - Los mensajes, archivos y llamadas están protegidos por **cifrado de extremo a extremo** con perfecta confidencialidad, repudio y recuperación tras ataques. + Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque. No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. - Los mensajes, archivos y llamadas están protegidos por **cifrado de extremo a extremo resistente a la computación cuántica** con perfecta confidencialidad, repudio y recuperación tras ataques. + Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo resistente a tecnología cuántica** con secreto perfecto hacía adelante, repudio y recuperación tras ataque. No comment provided by engineer. @@ -3655,12 +3710,12 @@ This is your link for group %@! Migrate to another device - Migrar hacia otro dispositivo + Migrar a otro dispositivo No comment provided by engineer. Migrate to another device via QR code. - Migrar hacia otro dispositivo mediante código QR. + Migrar a otro dispositivo mediante código QR. No comment provided by engineer. @@ -3718,6 +3773,11 @@ This is your link for group %@! ¡Pronto habrá más mejoras! No comment provided by engineer. + + More reliable network connection. + Conexión de red más fiable. + No comment provided by engineer. + Most likely this connection is deleted. Probablemente la conexión ha sido eliminada. @@ -3753,6 +3813,16 @@ This is your link for group %@! Servidores y Redes No comment provided by engineer. + + Network connection + Conexión de red + No comment provided by engineer. + + + Network management + Gestión de la red + No comment provided by engineer. + Network settings Configuración de red @@ -3863,6 +3933,11 @@ This is your link for group %@! Sin historial No comment provided by engineer. + + No network connection + Sin conexión de red + No comment provided by engineer. + No permission to record voice message Sin permiso para grabar mensajes de voz @@ -4039,7 +4114,7 @@ This is your link for group %@! Open migration to another device - Abrir la migración hacia otro dispositivo + Abrir menú migración a otro dispositivo authentication reason @@ -4069,7 +4144,7 @@ This is your link for group %@! Or securely share this file link - O comparta de forma segura el enlace de este archivo + O comparte de forma segura este enlace al archivo No comment provided by engineer. @@ -4077,6 +4152,11 @@ This is your link for group %@! O mostrar este código No comment provided by engineer. + + Other + Otro + No comment provided by engineer. + PING count Contador PING @@ -4184,7 +4264,7 @@ This is your link for group %@! Please confirm that network settings are correct for this device. - Por favor confirme que la configuración de red es correcta para este dispositivo. + Por favor, confirma que la configuración de red es correcta para este dispositivo. No comment provided by engineer. @@ -4244,11 +4324,6 @@ Error: %@ Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta server test error - - Post-quantum E2EE - E2EE postcuántica - No comment provided by engineer. - Preserve the last message draft, with attachments. Conserva el último borrador del mensaje con los datos adjuntos. @@ -4291,7 +4366,7 @@ Error: %@ Profile and server connections - Perfil y conexiones de servidor + Datos del perfil y conexiones No comment provided by engineer. @@ -4299,6 +4374,11 @@ Error: %@ Imagen del perfil No comment provided by engineer. + + Profile images + Imágenes del perfil + No comment provided by engineer. + Profile name Nombre del perfil @@ -4339,6 +4419,11 @@ Error: %@ No se permiten reacciones a los mensajes. No comment provided by engineer. + + Prohibit sending SimpleX links. + No permitir el envío de enlaces SimpleX. + No comment provided by engineer. + Prohibit sending direct messages to members. No se permiten mensajes directos entre miembros. @@ -4391,7 +4476,7 @@ Error: %@ Quantum resistant encryption - Cifrado resistente a la tecnología cuántica + Cifrado resistente a tecnología cuántica No comment provided by engineer. @@ -4469,6 +4554,11 @@ Error: %@ La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea. No comment provided by engineer. + + Receiving concurrency + Concurrencia en la recepción + No comment provided by engineer. + Receiving file will be stopped. Se detendrá la recepción del archivo. @@ -4484,6 +4574,11 @@ Error: %@ Historial reciente y [bot del directorio](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) mejorados. No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Los destinatarios no ven de quién procede este mensaje. + No comment provided by engineer. + Recipients see updates as you type them. Los destinatarios ven actualizarse mientras escribes. @@ -4596,7 +4691,7 @@ Error: %@ Repeat upload - Repetir la carga + Repetir subida No comment provided by engineer. @@ -4784,11 +4879,21 @@ Error: %@ ¿Guardar mensaje de bienvenida? No comment provided by engineer. + + Saved + Guardado + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Los servidores WebRTC ICE guardados serán eliminados No comment provided by engineer. + + Saved from + Guardado desde + No comment provided by engineer. + Saved message Mensaje guardado @@ -4881,7 +4986,7 @@ Error: %@ Send a live message - it will update for the recipient(s) as you type it - Envía un mensaje en vivo: se actualizará para el(los) destinatario(s) a medida que se escribe + Envía un mensaje en vivo: se actualizará para el (los) destinatario(s) a medida que se escribe No comment provided by engineer. @@ -5094,6 +5199,11 @@ Error: %@ Configuración No comment provided by engineer. + + Shape profile images + Dar forma a las imágenes de perfil + No comment provided by engineer. + Share Compartir @@ -5212,6 +5322,16 @@ Error: %@ SimpleX links Enlaces SimpleX + chat feature + + + SimpleX links are prohibited in this group. + Los enlaces SimpleX no se permiten en este grupo. + No comment provided by engineer. + + + SimpleX links not allowed + Enlaces SimpleX no permitidos No comment provided by engineer. @@ -5249,6 +5369,11 @@ Error: %@ Alguien notification title + + Square, circle, or anything in between. + Cuadrada, circular o cualquier forma intermedia. + No comment provided by engineer. + Start chat Iniciar chat @@ -5266,32 +5391,32 @@ Error: %@ Stop - Detener + Parar No comment provided by engineer. Stop SimpleX - Detener SimpleX + Parar SimpleX authentication reason Stop chat - Detener el chat + Parar chat No comment provided by engineer. Stop chat to enable database actions - Detén SimpleX para habilitar las acciones sobre la base de datos + Para habilitar las acciones sobre la base de datos, debes parar Chat No comment provided by engineer. Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - Para poder exportar, importar o eliminar la base de datos primero debes detener Chat. Durante el tiempo que esté detenido no podrás recibir ni enviar mensajes. + Para poder exportar, importar o eliminar la base de datos primero debes parar Chat. Mientras tanto no podrás recibir ni enviar mensajes. No comment provided by engineer. Stop chat? - ¿Detener Chat? + ¿Parar Chat? No comment provided by engineer. @@ -5321,7 +5446,7 @@ Error: %@ Stopping chat - Detención del chat + Parando chat No comment provided by engineer. @@ -5436,12 +5561,12 @@ Error: %@ Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! - Gracias a los usuarios: [contribuye vía Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)! + ¡Nuestro agradecimiento a todos los colaboradores, [puedes contribuir a través de Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)! No comment provided by engineer. Thanks to the users – contribute via Weblate! - ¡Gracias a los colaboradores! Contribuye a través de Weblate. + ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate No comment provided by engineer. @@ -5458,7 +5583,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The app can notify you when you receive messages or contact requests - please open settings to enable. - La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: abre la configuración para habilitar. + La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo. No comment provided by engineer. @@ -5563,7 +5688,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos. + Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Podría tardar varios minutos. No comment provided by engineer. @@ -5578,7 +5703,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This chat is protected by quantum resistant end-to-end encryption. - Este chat está protegido por un cifrado de extremo a extremo resistente a tecnologías cuánticas. + Este chat está protegido por cifrado de extremo a extremo resistente a tecnología cuántica. E2EE info chat item @@ -5878,7 +6003,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Upload failed - Error de carga + Error de subida No comment provided by engineer. @@ -5888,7 +6013,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Uploading archive - Subiendo el archivo + Subiendo archivo No comment provided by engineer. @@ -5943,7 +6068,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Use the app while in the call. - Usar la app durante la llamada. + Usar la aplicación durante la llamada. No comment provided by engineer. @@ -6013,12 +6138,12 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Video will be received when your contact completes uploading it. - El video se recibirá cuando tu contacto termine de subirlo. + El video se recibirá cuando el contacto termine de subirlo. No comment provided by engineer. Video will be received when your contact is online, please wait or check later! - El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde. + El vídeo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. No comment provided by engineer. @@ -6051,6 +6176,11 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Los mensajes de voz no están permitidos en este grupo. No comment provided by engineer. + + Voice messages not allowed + Mensajes de voz no permitidos + No comment provided by engineer. + Voice messages prohibited! ¡Mensajes de voz no permitidos! @@ -6083,7 +6213,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Warning: starting chat on multiple devices is not supported and will cause message delivery failures - Advertencia: el inicio del chat en varios dispositivos no es compatible y provocará fallos en la entrega de mensajes + Atención: el inicio del chat en varios dispositivos es incompatible y provocará fallos en la entrega de mensajes No comment provided by engineer. @@ -6108,7 +6238,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Welcome message is too long - El mensaje de bienvenida es demasiado largo + Mensaje de bienvenida demasiado largo No comment provided by engineer. @@ -6121,6 +6251,11 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Si disponibles No comment provided by engineer. + + When connecting audio and video calls. + Al iniciar llamadas de audio y vídeo. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud. @@ -6131,6 +6266,21 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + ¡Será habilitado en los chats directos! + No comment provided by engineer. + + + Wired ethernet + Ethernet por cable + No comment provided by engineer. + With encrypted files and media. Con cifrado de archivos y multimedia. @@ -6168,7 +6318,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb You **must not** use the same database on two devices. - **No debe** usar la misma base de datos en dos dispositivos. + **No debes** usar la misma base de datos en dos dispositivos. No comment provided by engineer. @@ -6260,7 +6410,7 @@ Repeat join request? You can give another try. - Puede intentarlo de nuevo. + Puedes intentarlo de nuevo. No comment provided by engineer. @@ -6407,7 +6557,7 @@ Repeat connection request? You will be connected when your contact's device is online, please wait or check later! - Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde. + Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o compruébalo más tarde. No comment provided by engineer. @@ -6493,8 +6643,8 @@ Repeat connection request? Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link). - Tu contacto debe estar en línea para que se complete la conexión. -Puedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo). + El contacto debe estar en línea para completar la conexión. +Puedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace nuevo). No comment provided by engineer. @@ -6614,6 +6764,11 @@ Los servidores de SimpleX no pueden ver tu perfil. administrador member role + + admins + administradores + feature role + agreeing encryption for %@… acordando cifrado para %@… @@ -6624,6 +6779,11 @@ Los servidores de SimpleX no pueden ver tu perfil. acordando cifrado… chat item text + + all members + todos los miembros + feature role + always siempre @@ -6661,12 +6821,12 @@ Los servidores de SimpleX no pueden ver tu perfil. blocked %@ - %@ bloqueado + ha bloqueado a %@ rcv group event chat item blocked by admin - bloqueado por el administrador + bloqueado por administrador marked deleted chat item preview text @@ -6954,6 +7114,11 @@ Los servidores de SimpleX no pueden ver tu perfil. evento ocurrido No comment provided by engineer. + + forwarded + reenviado + No comment provided by engineer. + group deleted grupo eliminado @@ -7161,6 +7326,11 @@ Los servidores de SimpleX no pueden ver tu perfil. propietario member role + + owners + propietarios + feature role + peer-to-peer p2p @@ -7168,7 +7338,7 @@ Los servidores de SimpleX no pueden ver tu perfil. quantum resistant e2e encryption - cifrado e2e resistente a la cuántica + cifrado e2e resistente a tecnología cuántica chat item text @@ -7211,6 +7381,16 @@ Los servidores de SimpleX no pueden ver tu perfil. te ha expulsado rcv group event chat item + + saved + guardado + No comment provided by engineer. + + + saved from %@ + Guardado desde %@ + No comment provided by engineer. + sec seg @@ -7268,7 +7448,7 @@ Los servidores de SimpleX no pueden ver tu perfil. unblocked %@ - %@ desbloqueado + ha desbloqueado a %@ rcv group event chat item @@ -7351,6 +7531,11 @@ Los servidores de SimpleX no pueden ver tu perfil. sí pref value + + you + tu + No comment provided by engineer. + you are invited to group has sido invitado a un grupo diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 3e971985ef..a239bebbcf 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -715,6 +715,10 @@ Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia) No comment provided by engineer. + + Allow to send SimpleX links. + No comment provided by engineer. + Allow to send files and media. Salli tiedostojen ja median lähettäminen. @@ -1036,6 +1040,10 @@ Tiedostoa ei voi vastaanottaa No comment provided by engineer. + + Cellular + No comment provided by engineer. + Change Muuta @@ -1992,6 +2000,10 @@ This cannot be undone! Alenna ja avaa keskustelu No comment provided by engineer. + + Download + chat item action + Download failed No comment provided by engineer. @@ -2097,6 +2109,10 @@ This cannot be undone! Ota itsetuhoava pääsykoodi käyttöön set passcode view + + Enabled for + No comment provided by engineer. + Encrypt Salaa @@ -2239,10 +2255,6 @@ This cannot be undone! Virhe lisättäessä jäseniä No comment provided by engineer. - - Error allowing contact PQ encryption - No comment provided by engineer. - Error changing address Virhe osoitteenvaihdossa @@ -2601,6 +2613,10 @@ This cannot be undone! Tiedostot ja media ovat tässä ryhmässä kiellettyjä. No comment provided by engineer. + + Files and media not allowed + No comment provided by engineer. + Files and media prohibited! Tiedostot ja media kielletty! @@ -2664,6 +2680,22 @@ This cannot be undone! Konsoliin No comment provided by engineer. + + Forward + chat item action + + + Forward and save messages + No comment provided by engineer. + + + Forwarded + No comment provided by engineer. + + + Forwarded from + No comment provided by engineer. + Found desktop No comment provided by engineer. @@ -2770,6 +2802,10 @@ This cannot be undone! Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia) No comment provided by engineer. + + Group members can send SimpleX links. + No comment provided by engineer. + Group members can send direct messages. Ryhmän jäsenet voivat lähettää suoraviestejä. @@ -3004,6 +3040,10 @@ This cannot be undone! Vastauksena No comment provided by engineer. + + In-call sounds + No comment provided by engineer. + Incognito Incognito @@ -3460,6 +3500,10 @@ This is your link for group %@! Viestireaktiot ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. + + Message source remains private. + No comment provided by engineer. + Message text Viestin teksti @@ -3564,6 +3608,10 @@ This is your link for group %@! Lisää parannuksia on tulossa pian! No comment provided by engineer. + + More reliable network connection. + No comment provided by engineer. + Most likely this connection is deleted. Todennäköisesti tämä yhteys on poistettu. @@ -3599,6 +3647,14 @@ This is your link for group %@! Verkko ja palvelimet No comment provided by engineer. + + Network connection + No comment provided by engineer. + + + Network management + No comment provided by engineer. + Network settings Verkkoasetukset @@ -3707,6 +3763,10 @@ This is your link for group %@! Ei historiaa No comment provided by engineer. + + No network connection + No comment provided by engineer. + No permission to record voice message Ei lupaa ääniviestin tallentamiseen @@ -3911,6 +3971,10 @@ This is your link for group %@! Or show this code No comment provided by engineer. + + Other + No comment provided by engineer. + PING count PING-määrä @@ -4070,10 +4134,6 @@ Error: %@ Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen server test error - - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Säilytä viimeinen viestiluonnos liitteineen. @@ -4123,6 +4183,10 @@ Error: %@ Profiilikuva No comment provided by engineer. + + Profile images + No comment provided by engineer. + Profile name No comment provided by engineer. @@ -4161,6 +4225,10 @@ Error: %@ Estä viestireaktiot. No comment provided by engineer. + + Prohibit sending SimpleX links. + No comment provided by engineer. + Prohibit sending direct messages to members. Estä suorien viestien lähettäminen jäsenille. @@ -4288,6 +4356,10 @@ Error: %@ Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon. No comment provided by engineer. + + Receiving concurrency + No comment provided by engineer. + Receiving file will be stopped. Tiedoston vastaanotto pysäytetään. @@ -4302,6 +4374,10 @@ Error: %@ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + No comment provided by engineer. + Recipients see updates as you type them. Vastaanottajat näkevät päivitykset, kun kirjoitat niitä. @@ -4595,11 +4671,19 @@ Error: %@ Tallenna tervetuloviesti? No comment provided by engineer. + + Saved + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Tallennetut WebRTC ICE -palvelimet poistetaan No comment provided by engineer. + + Saved from + No comment provided by engineer. + Saved message message info title @@ -4897,6 +4981,10 @@ Error: %@ Asetukset No comment provided by engineer. + + Shape profile images + No comment provided by engineer. + Share Jaa @@ -5013,6 +5101,14 @@ Error: %@ SimpleX links SimpleX-linkit + chat feature + + + SimpleX links are prohibited in this group. + No comment provided by engineer. + + + SimpleX links not allowed No comment provided by engineer. @@ -5049,6 +5145,10 @@ Error: %@ Joku notification title + + Square, circle, or anything in between. + No comment provided by engineer. + Start chat Aloita keskustelu @@ -5813,6 +5913,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Ääniviestit ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. + + Voice messages not allowed + No comment provided by engineer. + Voice messages prohibited! Ääniviestit kielletty! @@ -5880,6 +5984,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Kun saatavilla No comment provided by engineer. + + When connecting audio and video calls. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen. @@ -5890,6 +5998,18 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu. No comment provided by engineer. + + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + No comment provided by engineer. + + + Wired ethernet + No comment provided by engineer. + With encrypted files and media. No comment provided by engineer. @@ -6353,6 +6473,10 @@ SimpleX-palvelimet eivät näe profiiliasi. ylläpitäjä member role + + admins + feature role + agreeing encryption for %@… salauksesta sovitaan %@:lle… @@ -6363,6 +6487,10 @@ SimpleX-palvelimet eivät näe profiiliasi. hyväksyy salausta… chat item text + + all members + feature role + always aina @@ -6685,6 +6813,10 @@ SimpleX-palvelimet eivät näe profiiliasi. tapahtuma tapahtui No comment provided by engineer. + + forwarded + No comment provided by engineer. + group deleted ryhmä poistettu @@ -6891,6 +7023,10 @@ SimpleX-palvelimet eivät näe profiiliasi. omistaja member role + + owners + feature role + peer-to-peer vertais @@ -6938,6 +7074,14 @@ SimpleX-palvelimet eivät näe profiiliasi. poisti sinut rcv group event chat item + + saved + No comment provided by engineer. + + + saved from %@ + No comment provided by engineer. + sec sek @@ -7070,6 +7214,10 @@ SimpleX-palvelimet eivät näe profiiliasi. kyllä pref value + + you + No comment provided by engineer. + you are invited to group sinut on kutsuttu ryhmään diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 1e335c00b0..ac7fc8cc8b 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -743,6 +743,11 @@ Autoriser la suppression irréversible de messages envoyés. (24 heures) No comment provided by engineer. + + Allow to send SimpleX links. + Autorise l'envoi de liens SimpleX. + No comment provided by engineer. + Allow to send files and media. Permet l'envoi de fichiers et de médias. @@ -1083,6 +1088,11 @@ Impossible de recevoir le fichier No comment provided by engineer. + + Cellular + Cellulaire + No comment provided by engineer. + Change Changer @@ -2078,6 +2088,11 @@ Cette opération ne peut être annulée ! Rétrograder et ouvrir le chat No comment provided by engineer. + + Download + Télécharger + chat item action + Download failed Échec du téléchargement @@ -2188,6 +2203,11 @@ Cette opération ne peut être annulée ! Activer le code d'autodestruction set passcode view + + Enabled for + Activé pour + No comment provided by engineer. + Encrypt Chiffrer @@ -2338,11 +2358,6 @@ Cette opération ne peut être annulée ! Erreur lors de l'ajout de membre·s No comment provided by engineer. - - Error allowing contact PQ encryption - Erreur lors de la négociation du chiffrement PQ - No comment provided by engineer. - Error changing address Erreur de changement d'adresse @@ -2713,6 +2728,11 @@ Cette opération ne peut être annulée ! Les fichiers et les médias sont interdits dans ce groupe. No comment provided by engineer. + + Files and media not allowed + Fichiers et médias non autorisés + No comment provided by engineer. + Files and media prohibited! Fichiers et médias interdits ! @@ -2778,6 +2798,26 @@ Cette opération ne peut être annulée ! Pour la console No comment provided by engineer. + + Forward + Transférer + chat item action + + + Forward and save messages + Transférer et sauvegarder des messages + No comment provided by engineer. + + + Forwarded + Transféré + No comment provided by engineer. + + + Forwarded from + Transféré depuis + No comment provided by engineer. + Found desktop Bureau trouvé @@ -2888,6 +2928,11 @@ Cette opération ne peut être annulée ! Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures) No comment provided by engineer. + + Group members can send SimpleX links. + Les membres du groupe peuvent envoyer des liens SimpleX. + No comment provided by engineer. + Group members can send direct messages. Les membres du groupe peuvent envoyer des messages directs. @@ -3128,6 +3173,11 @@ Cette opération ne peut être annulée ! En réponse à No comment provided by engineer. + + In-call sounds + Sons d'appel + No comment provided by engineer. + Incognito Incognito @@ -3603,6 +3653,11 @@ Voici votre lien pour le groupe %@ ! Les réactions aux messages sont interdites dans ce groupe. No comment provided by engineer. + + Message source remains private. + La source du message reste privée. + No comment provided by engineer. + Message text Texte du message @@ -3718,6 +3773,11 @@ Voici votre lien pour le groupe %@ ! Plus d'améliorations à venir ! No comment provided by engineer. + + More reliable network connection. + Connexion réseau plus fiable. + No comment provided by engineer. + Most likely this connection is deleted. Connexion probablement supprimée. @@ -3753,6 +3813,16 @@ Voici votre lien pour le groupe %@ ! Réseau et serveurs No comment provided by engineer. + + Network connection + Connexion au réseau + No comment provided by engineer. + + + Network management + Gestion du réseau + No comment provided by engineer. + Network settings Paramètres réseau @@ -3863,6 +3933,11 @@ Voici votre lien pour le groupe %@ ! Aucun historique No comment provided by engineer. + + No network connection + Pas de connexion au réseau + No comment provided by engineer. + No permission to record voice message Pas l'autorisation d'enregistrer un message vocal @@ -4077,6 +4152,11 @@ Voici votre lien pour le groupe %@ ! Ou présenter ce code No comment provided by engineer. + + Other + Autres + No comment provided by engineer. + PING count Nombre de PING @@ -4244,11 +4324,6 @@ Erreur : %@ Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte server test error - - Post-quantum E2EE - E2EE post-quantique - No comment provided by engineer. - Preserve the last message draft, with attachments. Conserver le brouillon du dernier message, avec les pièces jointes. @@ -4299,6 +4374,11 @@ Erreur : %@ Image de profil No comment provided by engineer. + + Profile images + Images de profil + No comment provided by engineer. + Profile name Nom du profil @@ -4339,6 +4419,11 @@ Erreur : %@ Interdire les réactions aux messages. No comment provided by engineer. + + Prohibit sending SimpleX links. + Interdire l'envoi de liens SimpleX. + No comment provided by engineer. + Prohibit sending direct messages to members. Interdire l'envoi de messages directs aux membres. @@ -4469,6 +4554,11 @@ Erreur : %@ L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne. No comment provided by engineer. + + Receiving concurrency + Réception simultanée + No comment provided by engineer. + Receiving file will be stopped. La réception du fichier sera interrompue. @@ -4484,6 +4574,11 @@ Erreur : %@ Historique récent et amélioration du [bot annuaire](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Le(s) destinataire(s) ne peut(vent) pas voir de qui provient ce message. + No comment provided by engineer. + Recipients see updates as you type them. Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez. @@ -4784,11 +4879,21 @@ Erreur : %@ Enregistrer le message d'accueil ? No comment provided by engineer. + + Saved + Enregistré + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Les serveurs WebRTC ICE sauvegardés seront supprimés No comment provided by engineer. + + Saved from + Enregistré depuis + No comment provided by engineer. + Saved message Message enregistré @@ -5094,6 +5199,11 @@ Erreur : %@ Paramètres No comment provided by engineer. + + Shape profile images + Images de profil modelable + No comment provided by engineer. + Share Partager @@ -5212,6 +5322,16 @@ Erreur : %@ SimpleX links Liens SimpleX + chat feature + + + SimpleX links are prohibited in this group. + Les liens SimpleX sont interdits dans ce groupe. + No comment provided by engineer. + + + SimpleX links not allowed + Les liens SimpleX ne sont pas autorisés No comment provided by engineer. @@ -5249,6 +5369,11 @@ Erreur : %@ Quelqu'un notification title + + Square, circle, or anything in between. + Carré, circulaire, ou toute autre forme intermédiaire. + No comment provided by engineer. + Start chat Démarrer le chat @@ -6050,6 +6175,11 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Les messages vocaux sont interdits dans ce groupe. No comment provided by engineer. + + Voice messages not allowed + Les messages vocaux ne sont pas autorisés + No comment provided by engineer. + Voice messages prohibited! Messages vocaux interdits ! @@ -6120,6 +6250,11 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Quand disponible No comment provided by engineer. + + When connecting audio and video calls. + Lors des appels audio et vidéo. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Vous pouvez accepter ou refuser les demandes de contacts. @@ -6130,6 +6265,21 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Lorsque vous partagez un profil incognito avec quelqu'un, ce profil sera utilisé pour les groupes auxquels il vous invite. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Activé dans les discussions directes ! + No comment provided by engineer. + + + Wired ethernet + Ethernet câblé + No comment provided by engineer. + With encrypted files and media. Avec les fichiers et les médias chiffrés. @@ -6613,6 +6763,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. admin member role + + admins + admins + feature role + agreeing encryption for %@… négociation du chiffrement avec %@… @@ -6623,6 +6778,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. négociation du chiffrement… chat item text + + all members + tous les membres + feature role + always toujours @@ -6953,6 +7113,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. event happened No comment provided by engineer. + + forwarded + transféré + No comment provided by engineer. + group deleted groupe supprimé @@ -7160,6 +7325,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. propriétaire member role + + owners + propriétaires + feature role + peer-to-peer pair-à-pair @@ -7210,6 +7380,16 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. vous a retiré rcv group event chat item + + saved + enregistré + No comment provided by engineer. + + + saved from %@ + enregistré à partir de %@ + No comment provided by engineer. + sec sec @@ -7350,6 +7530,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. oui pref value + + you + vous + No comment provided by engineer. + you are invited to group vous êtes invité·e au groupe diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 546bdf23d3..ad3148d891 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -54,7 +54,7 @@ ## In reply to - ## Válaszul erre + ## Válaszul erre: copied message info @@ -94,26 +94,27 @@ %@ and %@ connected - %@ és %@ csatlakozott + %@ és %@ kapcsolódott No comment provided by engineer. %1$@ at %2$@: - %1$@ %2$@-kor: + %1$@ ekkor: %2$@ copied message info, <sender> at <time> %@ connected - %@ csatlakozott + %@ kapcsolódott No comment provided by engineer. %@ downloaded + %@ letöltve No comment provided by engineer. %@ is connected! - %@ csatlakozott! + %@ kapcsolódott! notification title @@ -133,11 +134,12 @@ %@ uploaded + %@ feltöltve No comment provided by engineer. %@ wants to connect! - %@ csatlakozni szeretne! + %@ kapcsolódni szeretne! notification title @@ -147,7 +149,7 @@ %@, %@ and %lld other members connected - %@, %@ és további %lld tag csatlakozott + %@, %@ és további %lld tag kapcsolódott No comment provided by engineer. @@ -182,7 +184,7 @@ %d skipped message(s) - %d kihagyott üzenet + %d üzenet kihagyva integrity error chat item @@ -237,7 +239,7 @@ %lld messages moderated by %@ - %@ %lld üzenetet moderált + %lld üzenet moderálva lett %@ által No comment provided by engineer. @@ -302,7 +304,7 @@ %u messages skipped. - %u kihagyott üzenet. + %u üzenet kihagyva. No comment provided by engineer. @@ -317,7 +319,7 @@ (this device v%@) - (ez az eszköz v%@) + (ez az eszköz: v%@) No comment provided by engineer. @@ -327,7 +329,7 @@ **Add contact**: to create a new invitation link, or connect via a link you received. - **Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő csatlakozáshoz. + **Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz. No comment provided by engineer. @@ -352,6 +354,7 @@ **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését. No comment provided by engineer. @@ -371,6 +374,7 @@ **Warning**: the archive will be removed. + **Figyelem**: az archívum törlésre kerül. No comment provided by engineer. @@ -397,7 +401,7 @@ - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable. - - kapcsolódás a [könyvtár szolgáltatáshoz] (simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Ld3%3DWpxkKFeXSPv3pwp %2F%3Fv%3D1-2%26dh %3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6glco6bqjETA)4Beklco6bqj) + - kapcsolódás a [könyvtár szolgáltatáshoz](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - kézbesítési jelentések (legfeljebb 20 tag). - gyorsabb és stabilabb. No comment provided by engineer. @@ -581,7 +585,7 @@ Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. - Azonosító hozzáadása a profilhoz, hogy az ismerősök megoszthassák másokkal. A profilfrissítés elküldésre kerül ismerősők számára. + Azonosító hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára. No comment provided by engineer. @@ -631,11 +635,12 @@ Admins can block a member for all. + Az adminok egy tagot mindenki számára letilthatnak. No comment provided by engineer. Admins can create the links to join groups. - Az adminok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. + Az adminok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz. No comment provided by engineer. @@ -650,7 +655,7 @@ All chats and messages will be deleted - this cannot be undone! - Minden csevegés és üzenet törlésre kerül - ez nem vonható vissza! + Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! No comment provided by engineer. @@ -660,17 +665,17 @@ All group members will remain connected. - Minden csoporttag csatlakoztatva marad. + Minden csoporttag kapcsolódva marad. No comment provided by engineer. All messages will be deleted - this cannot be undone! - Minden üzenet törlésre kerül – ez nem vonható vissza! + Minden üzenet törlésre kerül – ez a művelet nem vonható vissza! No comment provided by engineer. All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - Minden üzenet törlésre kerül - ezt nem vonható vissza! Az üzenetek CSAK az ön számára törlődnek. + Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az ön számára törlődnek. No comment provided by engineer. @@ -680,7 +685,7 @@ All your contacts will remain connected. - Minden ismerős csatlakoztatva marad. + Minden ismerős kapcsolódva marad. No comment provided by engineer. @@ -690,6 +695,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP átjátszókra. No comment provided by engineer. @@ -724,7 +730,7 @@ Allow sending direct messages to members. - Közvetlen üzenetek küldésének engedélyezése tagok részére. + Közvetlen üzenetek küldésének engedélyezése a tagok számára. No comment provided by engineer. @@ -734,7 +740,12 @@ Allow to irreversibly delete sent messages. (24 hours) - Elküldött üzenetek visszafordíthatatlan törlésének engedélyezése. (24 óra) + Elküldött üzenetek végleges törlésének engedélyezése. (24 óra) + No comment provided by engineer. + + + Allow to send SimpleX links. + A SimpleX hivatkozások küldése engedélyezve van. No comment provided by engineer. @@ -769,7 +780,7 @@ Allow your contacts to irreversibly delete sent messages. (24 hours) - Elküldött üzenetek visszafordíthatatlan törlésének engedélyezése ismerősök számára. (24 óra) + Elküldött üzenetek végleges törlésének engedélyezése az ismerősök számára. (24 óra) No comment provided by engineer. @@ -784,7 +795,7 @@ Already connected? - Csatlakoztatva? + Már kapcsolódott? No comment provided by engineer. @@ -794,7 +805,7 @@ Already joining the group! - Csatlakozás folyamatban! + A csatlakozás folyamatban van a csoporthoz! No comment provided by engineer. @@ -819,6 +830,7 @@ App data migration + Alkalmazásadatok átköltöztetése No comment provided by engineer. @@ -858,14 +870,17 @@ Apply + Alkalmaz No comment provided by engineer. Archive and upload + Archiválás és feltöltés No comment provided by engineer. Archiving database + Adatbázis archiválása No comment provided by engineer. @@ -900,12 +915,12 @@ Authentication failed - Hitelesítés sikertelen + Sikertelen hitelesítés No comment provided by engineer. Authentication is required before the call is connected, but you may miss calls. - A hívás csatlakoztatása előtt hitelesítésre van szükség, de előfordulhat, hogy nem tud hívásokat fogadni. + A hívás összekapcsolása előtt hitelesítésre van szükség, de előfordulhat, hogy lemarad a hívásokról. No comment provided by engineer. @@ -1000,7 +1015,7 @@ Both you and your contact can irreversibly delete sent messages. (24 hours) - Mindkét fél visszafordíthatatlanul törölheti az elküldött üzeneteket. (24 óra) + Mindkét fél törölheti véglegesen az elküldött üzeneteket. (24 óra) No comment provided by engineer. @@ -1055,11 +1070,12 @@ Cancel - Megszakítás + Mégse No comment provided by engineer. Cancel migration + Átköltöztetés visszavonása No comment provided by engineer. @@ -1072,6 +1088,11 @@ Nem lehet fogadni a fájlt No comment provided by engineer. + + Cellular + Mobilhálózat + No comment provided by engineer. + Change Változtatás @@ -1165,6 +1186,7 @@ Chat migrated! + A csevegés átköltöztetve! No comment provided by engineer. @@ -1189,6 +1211,7 @@ Choose _Migrate from another device_ on the new device and scan QR code. + Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközön és szkennelje be a QR-kódot. No comment provided by engineer. @@ -1263,6 +1286,7 @@ Confirm network settings + Hálózati beállítások megerősítése No comment provided by engineer. @@ -1277,10 +1301,12 @@ Confirm that you remember database passphrase to migrate it. + Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez. No comment provided by engineer. Confirm upload + Feltöltés megerősítése No comment provided by engineer. @@ -1295,7 +1321,7 @@ Connect incognito - Inkognítóban csatlakozva + Kapcsolódás inkognitóban No comment provided by engineer. @@ -1324,7 +1350,7 @@ Ez az egyszer használatos hivatkozása! Connect via contact address - Kapcsolódás ismerős azonosítója által + Kapcsolódás a kapcsolattartási azonosítón keresztül No comment provided by engineer. @@ -1349,7 +1375,7 @@ Ez az egyszer használatos hivatkozása! Connected to desktop - Csatlakozva a számítógéphez + Kapcsolódva a számítógéphez No comment provided by engineer. @@ -1414,12 +1440,12 @@ Ez az egyszer használatos hivatkozása! Contact is connected - Ismerős csatlakozott + Ismerőse kapcsolódott notification Contact is not connected yet! - Az ismerős még nem csatlakozott! + Az ismerőse még nem kapcsolódott! No comment provided by engineer. @@ -1529,7 +1555,7 @@ Ez az egyszer használatos hivatkozása! Created at - Létrehozva ekkor + Létrehozva ekkor: No comment provided by engineer. @@ -1544,6 +1570,7 @@ Ez az egyszer használatos hivatkozása! Creating archive link + Archív hivatkozás létrehozása No comment provided by engineer. @@ -1758,7 +1785,7 @@ Ez az egyszer használatos hivatkozása! Delete contact? This cannot be undone! Ismerős törlése? -Ezt nem vonható vissza! +Ez a művelet nem vonható vissza! No comment provided by engineer. @@ -1768,6 +1795,7 @@ Ezt nem vonható vissza! Delete database from this device + Adatbázis törlése erről az eszközről No comment provided by engineer. @@ -1877,7 +1905,7 @@ Ezt nem vonható vissza! Deleted at - Törölve ekkor + Törölve ekkor: No comment provided by engineer. @@ -1997,7 +2025,7 @@ Ezt nem vonható vissza! Disappears at - Eltűnik ekkor + Eltűnik ekkor: No comment provided by engineer. @@ -2037,7 +2065,7 @@ Ezt nem vonható vissza! Do not send history to new members. - Ne küldjön előzményeket új tagok részére. + Az előzmények ne kerüljenek elküldésre az új tagok számára. No comment provided by engineer. @@ -2060,8 +2088,14 @@ Ezt nem vonható vissza! Visszatérés a korábbi verzióra és a csevegés megnyitása No comment provided by engineer. + + Download + Letöltés + chat item action + Download failed + Sikertelen letöltés No comment provided by engineer. @@ -2071,10 +2105,12 @@ Ezt nem vonható vissza! Downloading archive + Archívum letöltése No comment provided by engineer. Downloading link details + Letöltési hivatkozás részletei No comment provided by engineer. @@ -2129,11 +2165,12 @@ Ezt nem vonható vissza! Enable for all - Engedélyezés mindenki részére + Engedélyezés mindenki számára No comment provided by engineer. Enable in direct chats (BETA)! + Engedélyezés a közvetlen csevegésekben (BÉTA)! No comment provided by engineer. @@ -2166,6 +2203,11 @@ Ezt nem vonható vissza! Önmegsemmisítő jelkód engedélyezése set passcode view + + Enabled for + Engedélyezve + No comment provided by engineer. + Encrypt Titkosít @@ -2208,7 +2250,7 @@ Ezt nem vonható vissza! Encrypted message: database migration error - Titkosított üzenet: adatbázis-migrációs hiba + Titkosított üzenet: adatbázis-átköltöztetés hiba notification @@ -2233,7 +2275,7 @@ Ezt nem vonható vissza! Encryption re-negotiation failed. - Titkosítás újraegyeztetése sikertelen. + Sikertelen titkosítás-újraegyeztetés. No comment provided by engineer. @@ -2253,6 +2295,7 @@ Ezt nem vonható vissza! Enter passphrase + Jelmondat megadása No comment provided by engineer. @@ -2287,7 +2330,7 @@ Ezt nem vonható vissza! Enter your name… - Adja meg nevét… + Adjon meg egy nevet… No comment provided by engineer. @@ -2315,10 +2358,6 @@ Ezt nem vonható vissza! Hiba a tag(-ok) hozzáadásakor No comment provided by engineer. - - Error allowing contact PQ encryption - No comment provided by engineer. - Error changing address Hiba az azonosító megváltoztatásakor @@ -2411,6 +2450,7 @@ Ezt nem vonható vissza! Error downloading the archive + Hiba az archívum letöltésekor No comment provided by engineer. @@ -2490,6 +2530,7 @@ Ezt nem vonható vissza! Error saving settings + Hiba a beállítások mentésekor when migrating @@ -2564,10 +2605,12 @@ Ezt nem vonható vissza! Error uploading the archive + Hiba az archívum feltöltésekor No comment provided by engineer. Error verifying passphrase: + Hiba a jelmondat ellenőrzésekor: No comment provided by engineer. @@ -2622,6 +2665,7 @@ Ezt nem vonható vissza! Exported file doesn't exist + Az exportált fájl nem létezik No comment provided by engineer. @@ -2656,12 +2700,12 @@ Ezt nem vonható vissza! File will be received when your contact completes uploading it. - A fájl akkor érkezik meg, amikor ismerőse befejezte annak feltöltést. + A fájl akkor érkezik meg, amikor a küldője befejezte annak feltöltését. No comment provided by engineer. File will be received when your contact is online, please wait or check later! - A fájl akkor érkezik meg, amint ismerőse online lesz, várjon, vagy ellenőrizze később! + A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. @@ -2684,6 +2728,11 @@ Ezt nem vonható vissza! A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban. No comment provided by engineer. + + Files and media not allowed + Fájlok és média tartalom küldése le van tiltva + No comment provided by engineer. + Files and media prohibited! A fájlok- és a médiatartalom küldése le van tiltva! @@ -2696,10 +2745,12 @@ Ezt nem vonható vissza! Finalize migration + Átköltöztetés véglegesítése No comment provided by engineer. Finalize migration on another device. + Átköltöztetés véglegesítése egy másik eszközön. No comment provided by engineer. @@ -2747,6 +2798,26 @@ Ezt nem vonható vissza! Konzolhoz No comment provided by engineer. + + Forward + Továbbítás + chat item action + + + Forward and save messages + Üzenetek továbbítása és mentése + No comment provided by engineer. + + + Forwarded + Továbbított + No comment provided by engineer. + + + Forwarded from + Továbbítva innen: + No comment provided by engineer. + Found desktop Megtalált számítógép @@ -2834,7 +2905,7 @@ Ezt nem vonható vissza! Group invitation is no longer valid, it was removed by sender. - A csoport meghívó már nem érvényes, el lett távolítva a küldője által. + A csoport meghívó már nem érvényes, a küldője törölte. No comment provided by engineer. @@ -2854,27 +2925,32 @@ Ezt nem vonható vissza! Group members can irreversibly delete sent messages. (24 hours) - Csoporttagok visszafordíthatatlanul törölhetik az elküldött üzeneteket. (24 óra) + A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + No comment provided by engineer. + + + Group members can send SimpleX links. + A csoport tagjai küldhetnek SimpleX hivatkozásokat. No comment provided by engineer. Group members can send direct messages. - Csoporttagok küldhetnek közvetlen üzeneteket. + A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. No comment provided by engineer. Group members can send disappearing messages. - Csoporttagok küldhetnek eltűnő üzeneteket. + A csoport tagjai küldhetnek eltűnő üzeneteket. No comment provided by engineer. Group members can send files and media. - Csoporttagok küldhetnek fájlokat és médiatartalmakat. + A csoport tagjai küldhetnek fájlokat és médiatartalmakat. No comment provided by engineer. Group members can send voice messages. - Csoporttagok küldhetnek hangüzeneteket. + A csoport tagjai küldhetnek hangüzeneteket. No comment provided by engineer. @@ -2909,12 +2985,12 @@ Ezt nem vonható vissza! Group will be deleted for all members - this cannot be undone! - Csoport törlésre kerül minden tag számára - ez nem vonható vissza! + Csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza! No comment provided by engineer. Group will be deleted for you - this cannot be undone! - A csoport törlésre kerül az ön részére - ez nem vonható vissza! + A csoport törlésre kerül az ön számára - ez a művelet nem vonható vissza! No comment provided by engineer. @@ -2964,7 +3040,7 @@ Ezt nem vonható vissza! History is not sent to new members. - Az előzmények nem kerülnek elküldésre új tagok részére. + Az előzmények nem kerülnek elküldésre az új tagok számára. No comment provided by engineer. @@ -2994,6 +3070,7 @@ Ezt nem vonható vissza! Hungarian interface + Magyar kezelőfelület No comment provided by engineer. @@ -3008,7 +3085,7 @@ Ezt nem vonható vissza! If you enter this passcode when opening the app, all app data will be irreversibly removed! - Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat visszafordíthatatlanul törlődik! + Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen törlődik! No comment provided by engineer. @@ -3028,12 +3105,12 @@ Ezt nem vonható vissza! Image will be received when your contact completes uploading it. - A kép akkor érkezik meg, amikor ismerőse befejezte annak feltöltését. + A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését. No comment provided by engineer. Image will be received when your contact is online, please wait or check later! - A kép akkor érkezik meg, amikor ismerős elérhető lesz, várjon vagy ellenőrizze később! + A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. @@ -3063,10 +3140,12 @@ Ezt nem vonható vissza! Import failed + Sikertelen importálás No comment provided by engineer. Importing archive + Archívum importálása No comment provided by engineer. @@ -3086,6 +3165,7 @@ Ezt nem vonható vissza! In order to continue, chat should be stopped. + A folytatáshoz a csevegést meg kell szakítani. No comment provided by engineer. @@ -3093,6 +3173,11 @@ Ezt nem vonható vissza! Válasz neki No comment provided by engineer. + + In-call sounds + Bejövő hívás csengőhangja + No comment provided by engineer. + Incognito Inkognitó @@ -3202,6 +3287,7 @@ Ezt nem vonható vissza! Invalid migration confirmation + Érvénytelen átköltöztetési visszaigazolás No comment provided by engineer. @@ -3246,17 +3332,17 @@ Ezt nem vonható vissza! Irreversible message deletion - Visszafordíthatatlan üzenettörlés + Végleges üzenettörlés No comment provided by engineer. Irreversible message deletion is prohibited in this chat. - Ebben a csevegésben az üzenetek visszafordíthatatlan törlése le van tiltva. + Ebben a csevegésben az üzenetek végleges törlése le van tiltva. No comment provided by engineer. Irreversible message deletion is prohibited in this group. - Ebben a csoportban az üzenetek visszafordíthatatlan törlése le van tiltva. + Ebben a csoportban az üzenetek végleges törlése le van tiltva. No comment provided by engineer. @@ -3282,7 +3368,7 @@ Ezt nem vonható vissza! It seems like you are already connected via this link. If it is not the case, there was an error (%@). - Úgy tűnik, már csatlakozott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@). + Úgy tűnik, már kapcsolódott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@). No comment provided by engineer. @@ -3474,7 +3560,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Make profile private! - Tegye priváttá profilját! + Tegye priváttá a profilját! No comment provided by engineer. @@ -3534,7 +3620,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Member will be removed from group - this cannot be undone! - A tag eltávolítása a csoportból - ez nem vonható vissza! + A tag eltávolítása a csoportból - ez a művelet nem vonható vissza! No comment provided by engineer. @@ -3567,6 +3653,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Ebben a csoportban az üzenetreakciók le vannak tiltva. No comment provided by engineer. + + Message source remains private. + Az üzenet forrása titokban marad. + No comment provided by engineer. + Message text Üzenet szövege @@ -3574,6 +3665,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message too large + Az üzenet túl nagy No comment provided by engineer. @@ -3593,34 +3685,42 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. + Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi. No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. + Az üzeneteket, fájlokat és hívásokat **végpontok közötti kvantumrezisztens titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi. No comment provided by engineer. Migrate device + Eszköz átköltöztetése No comment provided by engineer. Migrate from another device + Átköltöztetés egy másik eszközről No comment provided by engineer. Migrate here + Átköltöztetés ide No comment provided by engineer. Migrate to another device + Átköltöztetés egy másik eszközre No comment provided by engineer. Migrate to another device via QR code. + Átköltöztetés egy másik eszközre QR-kód használatával. No comment provided by engineer. Migrating + Átköltöztetés No comment provided by engineer. @@ -3630,26 +3730,27 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Migration complete + Átköltöztetés befejezve No comment provided by engineer. Migration error: - Migrációs hiba: + Átköltöztetés hiba: No comment provided by engineer. Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - Sikertelen migráció. Koppintson a **Kihagyás** lehetőségre az aktuális adatbázis használatának folytatásához. Kérjük, jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat). + Sikertelen átköltöztetés. Koppintson a **Kihagyás** lehetőségre az aktuális adatbázis használatának folytatásához. Jelentse a problémát az alkalmazás fejlesztőinek csevegésben vagy e-mailben [chat@simplex.chat](mailto:chat@simplex.chat). No comment provided by engineer. Migration is completed - A migráció befejeződött + Az átköltöztetés befejeződött No comment provided by engineer. Migrations: %@ - Migrációk: %@ + Átköltöztetések: %@ No comment provided by engineer. @@ -3659,12 +3760,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Moderated at - Moderálva ekkor + Moderálva lett ekkor: No comment provided by engineer. Moderated at: %@ - Moderálva ekkor: %@ + Moderálva lett ekkor: %@ copied message info @@ -3672,6 +3773,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Hamarosan további fejlesztések érkeznek! No comment provided by engineer. + + More reliable network connection. + Megbízhatóbb hálózati kapcsolat. + No comment provided by engineer. + Most likely this connection is deleted. Valószínűleg ez a kapcsolat törlésre került. @@ -3707,6 +3813,16 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Hálózat és kiszolgálók No comment provided by engineer. + + Network connection + Internetkapcsolat + No comment provided by engineer. + + + Network management + Hálózatkezelés + No comment provided by engineer. + Network settings Hálózati beállítások @@ -3817,6 +3933,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nincsenek előzmények No comment provided by engineer. + + No network connection + Nincs hálózati kapcsolat + No comment provided by engineer. + No permission to record voice message Nincs engedély a hangüzenet rögzítésére @@ -3883,17 +4004,17 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Onion hosts will be required for connection. Requires enabling VPN. - A csatlakozáshoz Onion host-okra lesz szükség. VPN engedélyezése szükséges. + A kapcsolódáshoz Onion kiszolgálókra lesz szükség. VPN engedélyezése szükséges. No comment provided by engineer. Onion hosts will be used when available. Requires enabling VPN. - Onion host-ok használata, ha azok rendelkezésre állnak. VPN engedélyezése szükséges. + Onion kiszolgálók használata, ha azok rendelkezésre állnak. VPN engedélyezése szükséges. No comment provided by engineer. Onion hosts will not be used. - Onion host-ok nem lesznek használva. + Onion kiszolgálók nem lesznek használva. No comment provided by engineer. @@ -3923,7 +4044,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours) - Visszafordíthatatlanul csak ön törölhet üzeneteket (ismerőse csak törlésre jelölheti őket ). (24 óra) + Véglegesen csak ön törölhet üzeneteket (ismerőse csak törlésre jelölheti meg őket ). (24 óra) No comment provided by engineer. @@ -3948,7 +4069,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours) - Csak az ismerős tud visszafordíthatatlanul törölni üzeneteket (megjelölheti őket törlésre). (24 óra) + Csak az ismerőse tudja az üzeneteket véglegesen törölni (ön csak törlésre jelölheti meg azokat). (24 óra) No comment provided by engineer. @@ -3993,6 +4114,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Open migration to another device + Átköltöztetés megkezdése egy másik eszközre authentication reason @@ -4012,6 +4134,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Or paste archive link + Vagy az archívum hivatkozásának beillesztése No comment provided by engineer. @@ -4021,6 +4144,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Or securely share this file link + Vagy a fájl hivítkozásának biztonságos megosztása No comment provided by engineer. @@ -4028,6 +4152,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Vagy mutassa meg ezt a kódot No comment provided by engineer. + + Other + További + No comment provided by engineer. + PING count PING számláló @@ -4065,7 +4194,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Password to show - Jelszó mutatása + Jelszó megjelenítése No comment provided by engineer. @@ -4085,7 +4214,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Paste link to connect! - Hivatkozás beillesztése a csatlakozáshoz! + Hivatkozás beillesztése a kapcsolódáshoz! No comment provided by engineer. @@ -4110,6 +4239,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Picture-in-picture calls + Kép a képben hívások No comment provided by engineer. @@ -4124,7 +4254,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Please check your network connection with %@ and try again. - Kérjük, ellenőrizze hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra. + Ellenőrizze hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra. No comment provided by engineer. @@ -4134,6 +4264,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Please confirm that network settings are correct for this device. + Ellenőrizze, hogy a hálózati beállítások megfelelőek-e ehhez az eszközhöz. No comment provided by engineer. @@ -4155,7 +4286,7 @@ Hiba: %@ Please enter the previous password after restoring database backup. This action can not be undone. - Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem visszavonható. + Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza. No comment provided by engineer. @@ -4170,7 +4301,7 @@ Hiba: %@ Please restart the app and migrate the database to enable push notifications. - Indítsa újra az alkalmazást az adatbázis-migrációhoz szükséges push értesítések engedélyezéséhez. + Indítsa újra az alkalmazást az adatbázis-átköltöztetéséhez szükséges push értesítések engedélyezéséhez. No comment provided by engineer. @@ -4193,10 +4324,6 @@ Hiba: %@ Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen server test error - - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. @@ -4247,6 +4374,11 @@ Hiba: %@ Profilkép No comment provided by engineer. + + Profile images + Profilképek + No comment provided by engineer. + Profile name Profilnév @@ -4287,9 +4419,14 @@ Hiba: %@ Az üzenetreakciók tiltása. No comment provided by engineer. + + Prohibit sending SimpleX links. + A SimpleX hivatkozások küldése le van tiltva. + No comment provided by engineer. + Prohibit sending direct messages to members. - Közvetlen üzenetek küldésének letiltása tagok részére. + Közvetlen üzenetek küldésének letiltása a tagok számára. No comment provided by engineer. @@ -4309,7 +4446,7 @@ Hiba: %@ Protect app screen - App képernyőjének védelme + Alkalmazás képernyőjének védelme No comment provided by engineer. @@ -4334,10 +4471,12 @@ Hiba: %@ Push server + Push kiszolgáló No comment provided by engineer. Quantum resistant encryption + Kvantumrezisztens titkosítás No comment provided by engineer. @@ -4362,17 +4501,17 @@ Hiba: %@ Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - További információ a [Felhasználói útmutatóban](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). + További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - További információ a [Felhasználói útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - További információ a [Felhasználói útmutatóban](https://simplex.chat/docs/guide/readme.html#connect-to-friends). + További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. @@ -4392,7 +4531,7 @@ Hiba: %@ Received at - Fogadva ekkor + Fogadva ekkor: No comment provided by engineer. @@ -4415,6 +4554,11 @@ Hiba: %@ A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be. No comment provided by engineer. + + Receiving concurrency + Egyidejű fogadás + No comment provided by engineer. + Receiving file will be stopped. A fájl fogadása leállt. @@ -4427,7 +4571,12 @@ Hiba: %@ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). - Legutóbbi előzmények és továbbfejlesztett [könyvtárbot] (simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2TxW3dfMfxy 3%23%2F%3Fv%3D1-2% 26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gloncbqjek4gloncbqjek. + Legutóbbi előzmények és továbbfejlesztett [könyvtárbot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). + No comment provided by engineer. + + + Recipient(s) can't see who this message is from. + A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet. No comment provided by engineer. @@ -4437,12 +4586,12 @@ Hiba: %@ Reconnect all connected servers to force message delivery. It uses additional traffic. - Az összes csatlakoztatott kiszolgáló újrakapcsolása az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ. + Újrakapcsolódás az összes kiszolgálóhoz az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ. No comment provided by engineer. Reconnect servers? - Kiszolgálók újracsatlakoztatása? + Újrakapcsolódás a kiszolgálókhoz? No comment provided by engineer. @@ -4527,10 +4676,12 @@ Hiba: %@ Repeat download + Letöltés ismét No comment provided by engineer. Repeat import + Importálás ismét No comment provided by engineer. @@ -4540,6 +4691,7 @@ Hiba: %@ Repeat upload + Feltöltés ismét No comment provided by engineer. @@ -4644,6 +4796,7 @@ Hiba: %@ Safer groups + Biztonságosabb csoportok No comment provided by engineer. @@ -4726,11 +4879,21 @@ Hiba: %@ Üdvözlőszöveg mentése? No comment provided by engineer. + + Saved + Mentett + No comment provided by engineer. + Saved WebRTC ICE servers will be removed A mentett WebRTC ICE kiszolgálók eltávolításra kerülnek No comment provided by engineer. + + Saved from + Mentve innen: + No comment provided by engineer. + Saved message Mentett üzenet @@ -4768,7 +4931,7 @@ Hiba: %@ Search bar accepts invitation links. - A keresősáv fogadja a meghívó hivatkozásokat. + A keresősáv elfogadja a meghívó hivatkozásokat. No comment provided by engineer. @@ -4838,7 +5001,7 @@ Hiba: %@ Send direct message to connect - A kapcsolódáshoz közvetlen üzenet küldése + Közvetlen üzenet küldése a kapcsolódáshoz No comment provided by engineer. @@ -4883,7 +5046,7 @@ Hiba: %@ Send up to 100 last messages to new members. - Utolsó 100 üzenet küldése új tagoknak. + Az utolsó 100 üzenet elküldése az új tagoknak. No comment provided by engineer. @@ -4938,7 +5101,7 @@ Hiba: %@ Sent at - Elküldve ekkor + Elküldve ekkor: No comment provided by engineer. @@ -4973,7 +5136,7 @@ Hiba: %@ Server test failed! - A kiszolgáló tesztje sikertelen! + Sikertelen kiszolgáló-teszt! No comment provided by engineer. @@ -5013,6 +5176,7 @@ Hiba: %@ Set passphrase + Jelmondat beállítása No comment provided by engineer. @@ -5022,7 +5186,7 @@ Hiba: %@ Set the message shown to new members! - Megjelenő üzenetet beállítása új tagok részére! + Megjelenő üzenet beállítása az új tagok számára! No comment provided by engineer. @@ -5035,6 +5199,11 @@ Hiba: %@ Beállítások No comment provided by engineer. + + Shape profile images + Profilkép alakzat + No comment provided by engineer. + Share Megosztás @@ -5052,7 +5221,7 @@ Hiba: %@ Share address with contacts? - Megosztja az azonosítót az ismerősökkel? + Megosztja az azonosítót az ismerőseivel? No comment provided by engineer. @@ -5072,6 +5241,7 @@ Hiba: %@ Show QR code + QR-kód megjelenítése No comment provided by engineer. @@ -5081,7 +5251,7 @@ Hiba: %@ Show developer options - Fejlesztői beállítások mutatása + Fejlesztői beállítások megjelenítése No comment provided by engineer. @@ -5096,7 +5266,7 @@ Hiba: %@ Show: - Mutat: + Megjelenítés: No comment provided by engineer. @@ -5136,7 +5306,7 @@ Hiba: %@ SimpleX contact address - SimpleX ismerős azonosítója + SimpleX kapcsolattartási azonosító simplex link type @@ -5152,6 +5322,16 @@ Hiba: %@ SimpleX links SimpleX hivatkozások + chat feature + + + SimpleX links are prohibited in this group. + A SimpleX hivatkozások küldése ebben a csoportban le van tiltva. + No comment provided by engineer. + + + SimpleX links not allowed + A SimpleX hivatkozások küldése le van tiltva No comment provided by engineer. @@ -5189,6 +5369,11 @@ Hiba: %@ Valaki notification title + + Square, circle, or anything in between. + Négyzet, kör vagy bármi a kettő között. + No comment provided by engineer. + Start chat Csevegés indítása @@ -5201,7 +5386,7 @@ Hiba: %@ Start migration - Migráció indítása + Átköltöztetés indítása No comment provided by engineer. @@ -5211,21 +5396,22 @@ Hiba: %@ Stop SimpleX - A SimpleX megállítása + SimpleX megállítása authentication reason Stop chat + Csevegési szolgáltatás megállítása No comment provided by engineer. Stop chat to enable database actions - Csevegés leállítása az adatbázis-műveletek engedélyezéséhez + Csevegés megállítása az adatbázis-műveletek engedélyezéséhez No comment provided by engineer. Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - A csevegés leállítása a csevegőadatbázis exportálásához, importálásához vagy törléséhez. A csevegés leállítása alatt nem tud üzeneteket fogadni és küldeni. + A csevegés megállítása a csevegő adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni. No comment provided by engineer. @@ -5240,17 +5426,17 @@ Hiba: %@ Stop receiving file? - Fájl fogadás megszakítása? + Fájl fogadás megállítása? No comment provided by engineer. Stop sending file? - Fájl küldés megszakítása? + Fájl küldés megállítása? No comment provided by engineer. Stop sharing - Megosztás leállítása + Megosztás megállítása No comment provided by engineer. @@ -5260,6 +5446,7 @@ Hiba: %@ Stopping chat + Csevegés megállítása folyamatban No comment provided by engineer. @@ -5309,12 +5496,12 @@ Hiba: %@ Tap button - Koppintson a gombra + Koppintson a No comment provided by engineer. Tap to Connect - Koppintson a csatlakozáshoz + Koppintson a kapcsolódáshoz No comment provided by engineer. @@ -5329,7 +5516,7 @@ Hiba: %@ Tap to join incognito - Koppintson az inkognitómódhoz való csatlakozáshoz + Koppintson az inkognitóban való csatlakozáshoz No comment provided by engineer. @@ -5416,7 +5603,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The contact you shared this link with will NOT be able to connect! - Ismerőse NEM fog tudni csatlakozni, akivel megosztotta ezt a hivatkozást! + Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! No comment provided by engineer. @@ -5451,12 +5638,12 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The old database was not removed during the migration, it can be deleted. - A régi adatbázis nem került eltávolításra a migráció során, így törölhető. + A régi adatbázis nem került eltávolításra az átköltöztetés során, így törölhető. No comment provided by engineer. The profile is only shared with your contacts. - Profilja csak az ismerősök számára kerül megosztásra. + Profilja csak az ismerőseivel kerül megosztásra. No comment provided by engineer. @@ -5506,15 +5693,17 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - Ez a művelet nem vonható vissza - profilok, ismerősök, üzenetek és fájlok visszafordíthatatlanul törlésre kerülnek. + Ez a művelet nem vonható vissza - profiljai, ismerősei, üzenetei és fájljai visszafordíthatatlanul törlésre kerülnek. No comment provided by engineer. This chat is protected by end-to-end encryption. + Ez a csevegés végpontok közötti titkosítással védett. E2EE info chat item This chat is protected by quantum resistant end-to-end encryption. + Ez a csevegés végpontok közötti kvantumrezisztens tikosítással védett. E2EE info chat item @@ -5559,7 +5748,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. To connect, your contact can scan QR code or use the link in the app. - A csatlakozáshoz az ismerős beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást. + A kapcsolódáshoz az ismerőse beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást. No comment provided by engineer. @@ -5611,7 +5800,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Toggle incognito when connecting. - Inkognító mód csatlakozáskor. + Inkognitó mód kapcsolódáskor. No comment provided by engineer. @@ -5621,12 +5810,12 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Trying to connect to the server used to receive messages from this contact (error: %@). - Csatlakozási kísérlet a kapcsolat üzeneteinek fogadására használt kiszolgálóhoz (hiba: %@). + Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %@). No comment provided by engineer. Trying to connect to the server used to receive messages from this contact. - Csatlakozási kísérlet a kapcsolat üzeneteinek fogadására használt kiszolgálóhoz ettől az ismerőstől. + Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. No comment provided by engineer. @@ -5681,7 +5870,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Unexpected migration state - Váratlan migrációs állapot + Váratlan átköltöztetési állapot No comment provided by engineer. @@ -5732,8 +5921,8 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Hacsak az ismerős nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, hiba lehet – kérjük, jelentse. -A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. + Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát. +A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. No comment provided by engineer. @@ -5768,7 +5957,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Up to 100 last messages are sent to new members. - Legfeljebb az utolsó 100 üzenet kerül elküldésre az új tagoknak. + Legfeljebb az utolsó 100 üzenet kerül elküldésre az új tagok számára. No comment provided by engineer. @@ -5778,7 +5967,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Update .onion hosts setting? - Tor .onion host beállítások frissítése? + Tor .onion kiszolgálók beállításainak frissítése? No comment provided by engineer. @@ -5803,7 +5992,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Updating this setting will re-connect the client to all servers. - A beállítás frissítésével a kliens újracsatlakozik az összes kiszolgálóhoz. + A beállítás frissítésével a kliens újrakapcsolódik az összes kiszolgálóhoz. No comment provided by engineer. @@ -5813,6 +6002,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Upload failed + Sikertelen feltöltés No comment provided by engineer. @@ -5822,11 +6012,12 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Uploading archive + Archívum feltöltése No comment provided by engineer. Use .onion hosts - Tor .onion hostok használata + Tor .onion kiszolgálók használata No comment provided by engineer. @@ -5876,6 +6067,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Use the app while in the call. + Használja az alkalmazást hívás közben. No comment provided by engineer. @@ -5885,7 +6077,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Using .onion hosts requires compatible VPN provider. - A .onion hosztok használatához kompatibilis VPN szolgáltatóra van szükség. + A .onion kiszolgálók használatához kompatibilis VPN szolgáltatóra van szükség. No comment provided by engineer. @@ -5915,10 +6107,12 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Verify database passphrase + Adatbázis jelmondatának ellenőrzése No comment provided by engineer. Verify passphrase + Jelmondat ellenőrzése No comment provided by engineer. @@ -5943,12 +6137,12 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Video will be received when your contact completes uploading it. - A videó akkor érkezik meg, amikor az ismerőse befejezte annak feltöltését. + A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. No comment provided by engineer. Video will be received when your contact is online, please wait or check later! - A videó akkor érkezik meg, amikor az ismerős elérhető, várjon, vagy ellenőrizze később! + A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. @@ -5981,6 +6175,11 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola A hangüzenetek küldése le van tiltva ebben a csoportban. No comment provided by engineer. + + Voice messages not allowed + A hangüzenetek küldése le van tiltva + No comment provided by engineer. + Voice messages prohibited! A hangüzenetek le vannak tilva! @@ -6013,6 +6212,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Warning: starting chat on multiple devices is not supported and will cause message delivery failures + Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, továbbá üzenetkézbesítési hibákat okozhat No comment provided by engineer. @@ -6037,6 +6237,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Welcome message is too long + Az üdvözlő üzenet túl hosszú No comment provided by engineer. @@ -6049,9 +6250,14 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Amikor elérhető No comment provided by engineer. + + When connecting audio and video calls. + Amikor egy bejövő hang- vagy videóhívás érkezik. + No comment provided by engineer. + When people request to connect, you can accept or reject it. - Csatlakozási kérelmek esetében, elfogadhatja vagy elutasíthatja azokat. + Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat. No comment provided by engineer. @@ -6059,6 +6265,21 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. No comment provided by engineer. + + WiFi + Wi-Fi + No comment provided by engineer. + + + Will be enabled in direct chats! + A közvetlen csevegésekben engedélyezve lesz! + No comment provided by engineer. + + + Wired ethernet + Vezetékes Ethernet + No comment provided by engineer. + With encrypted files and media. Titkosított fájlokkal és médiatartalommal. @@ -6096,6 +6317,7 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola You **must not** use the same database on two devices. + **Nem szabad** ugyanazt az adatbázist használni egyszerre két eszközön. No comment provided by engineer. @@ -6115,37 +6337,37 @@ A csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsola You are already connected to %@. - Már csatlakozva van ehhez: %@. + Már kapcsolódva van hozzá: %@. No comment provided by engineer. You are already connecting to %@. - Már folyamatban van a csatlakozás ehhez: %@. + Már folyamatban van a kapcsolódás ehhez: %@. No comment provided by engineer. You are already connecting via this one-time link! - Már csatlakozik ezen az egyszer használatos hivatkozáson keresztül! + A kapcsolódás már folyamatban van ezen az egyszer használatos hivatkozáson keresztül! No comment provided by engineer. You are already in group %@. - Már a %@ csoportban van. + Már a(z) %@ csoport tagja. No comment provided by engineer. You are already joining the group %@. - Már folyamatban van a csatlakozás a csoporthoz %@. + A csatlakozás már folyamatban van a(z) %@ csoporthoz. No comment provided by engineer. You are already joining the group via this link! - Már csatlakozott a csoporthoz ezen a hivatkozáson keresztül! + A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül! No comment provided by engineer. You are already joining the group via this link. - Ezen a hivatkozáson keresztül már csatlakozik a csoporthoz. + A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. No comment provided by engineer. @@ -6157,7 +6379,7 @@ Csatlakozási kérés megismétlése? You are connected to the server used to receive messages from this contact. - Kiszolgálóhoz történő csatlakozás, mely az adott ismerőstől érkező üzenetek fogadására szolgál. + Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. No comment provided by engineer. @@ -6187,6 +6409,7 @@ Csatlakozási kérés megismétlése? You can give another try. + Megpróbálhatja még egyszer. No comment provided by engineer. @@ -6216,12 +6439,12 @@ Csatlakozási kérés megismétlése? You can share this address with your contacts to let them connect with **%@**. - Megoszthatja ezt a hivatkozást ismerőseivel, hogy kapcsolatba léphessenek önnel a **%@** nevű profilján keresztül. + Megoszthatja ezt az azonosítót az ismerőseivel, hogy kapcsolatba léphessenek önnel a **%@** nevű profilján keresztül. No comment provided by engineer. You can share your address as a link or QR code - anybody can connect to you. - Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki csatlakozhat önhöz. + Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz. No comment provided by engineer. @@ -6261,13 +6484,13 @@ Csatlakozási kérés megismétlése? You have already requested connection via this address! - Már kért egy csatlakozást ezen az azonosítón keresztül! + Már kért egy kapcsolódási kérelmet ezen az azonosítón keresztül! No comment provided by engineer. You have already requested connection! Repeat connection request? - Már kérelmezte a csatlakozást! + Már kért egy kapcsolódási kérelmet! Kapcsolódási kérés megismétlése? No comment provided by engineer. @@ -6283,7 +6506,7 @@ Kapcsolódási kérés megismétlése? You invited a contact - Meghívott egy ismerőst + Meghívta egy ismerősét No comment provided by engineer. @@ -6318,22 +6541,22 @@ Kapcsolódási kérés megismétlése? You will be connected to group when the group host's device is online, please wait or check later! - Akkor tud csatlakozni a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. You will be connected when group link host's device is online, please wait or check later! - Akkor lesz csatlakoztatva, amikor a csoportos hivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, amikor a csoportos hivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. You will be connected when your connection request is accepted, please wait or check later! - Akkor lesz csatlakoztatva, ha a csatlakozási kérelme elfogadásra került, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, ha a kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később! No comment provided by engineer. You will be connected when your contact's device is online, please wait or check later! - Akkor csatlakozik, amikor az ismerős eszköze online lesz, várjon, vagy ellenőrizze később! + Akkor le kapcsolódva, amikor az ismerőse eszköze online lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. @@ -6343,7 +6566,7 @@ Kapcsolódási kérés megismétlése? You will connect to all group members. - Csatlakozni fog a csoport összes tagjához. + Kapcsolódni fog a csoport összes tagjához. No comment provided by engineer. @@ -6358,7 +6581,7 @@ Kapcsolódási kérés megismétlése? You won't lose your contacts if you later delete your address. - Nem veszíti el ismerőseit, ha később törli az azonosítóját. + Nem veszíti el az ismerőseit, ha később törli az azonosítóját. No comment provided by engineer. @@ -6403,12 +6626,12 @@ Kapcsolódási kérés megismétlése? Your chat database - Csevegési adatbázisa + Csevegési adatbázis No comment provided by engineer. Your chat database is not encrypted - set passphrase to encrypt it. - Csevegési adatbázisa nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz. + A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz. No comment provided by engineer. @@ -6435,7 +6658,7 @@ Megszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt kés Your contacts will remain connected. - Az ismerősök továbbra is csatlakoztatva maradnak. + Az ismerősei továbbra is kapcsolódva maradnak. No comment provided by engineer. @@ -6465,7 +6688,7 @@ Megszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt kés Your profile **%@** will be shared. - **%@** nevű profilja megosztásra kerül. + A(z) **%@** nevű profilja megosztásra fog kerülni. No comment provided by engineer. @@ -6477,7 +6700,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. Your profile, contacts and delivered messages are stored on your device. - Profilja, ismerősök és az elküldött üzenetek az eszközön kerülnek tárolásra. + Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra. No comment provided by engineer. @@ -6527,7 +6750,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. above, then choose: - fent, majd válassza ki: + gombra fent, majd válassza ki: No comment provided by engineer. @@ -6540,6 +6763,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. admin member role + + admins + adminok + feature role + agreeing encryption for %@… titkosítás jóváhagyása %@ számára… @@ -6550,6 +6778,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. titkosítás elfogadása… chat item text + + all members + minden tag + feature role + always mindig @@ -6557,7 +6790,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. and %lld other events - és %lld egyéb esemény + és %lld további esemény No comment provided by engineer. @@ -6622,12 +6855,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. changed address for you - Cím megváltoztatva + cím megváltoztatva chat item text changed role of %1$@ to %2$@ - %1$@ szerepköre megváltozott erre: %2$@ + %1$@ szerepkörét megváltoztatta erre: %2$@ rcv group event chat item @@ -6657,7 +6890,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. connect to SimpleX Chat developers. - Csatlakozás a SimpleX Chat fejlesztőkhöz. + Kapcsolódás a SimpleX Chat fejlesztőkhöz. No comment provided by engineer. @@ -6692,7 +6925,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. connecting (introduction invitation) - csatlakozás (bemutatkozás meghívás) + kapcsolódás (bemutatkozó meghívó) No comment provided by engineer. @@ -6707,7 +6940,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. connection established - Kapcsolat létrehozva + kapcsolat létrehozva chat list item title (it should not be shown @@ -6717,7 +6950,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. contact %1$@ changed to %2$@ - %1$@ ismerősének neve megváltozott erre: %2$@ + %1$@ megváltoztatta a nevét erre: %2$@ profile update event chat item @@ -6742,7 +6975,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. database version is newer than the app, but no down migration for: %@ - az adatbázis verziója újabb, mint az alkalmazásé, de nincs visszafelé migráció: %@ + az adatbázis verziója újabb, mint az alkalmazásé, de nincs visszafelé átköltöztetés ehhez: %@ No comment provided by engineer. @@ -6782,7 +7015,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. different migration in the app/database: %@ / %@ - különböző migrációk az alkalmazásban/adatbázisban: %@ / %@ + különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@ No comment provided by engineer. @@ -6812,7 +7045,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. enabled for contact - engedélyezve ismerős részére + engedélyezve az ismerős számára enabled status @@ -6880,6 +7113,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. esemény történt No comment provided by engineer. + + forwarded + továbbított + No comment provided by engineer. + group deleted a csoport törölve @@ -6917,7 +7155,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. incognito via one-time link - inkognitó egyszer használatos hivatkozáson keresztül + inkognitó az egyszer használatos hivatkozáson keresztül chat list item description @@ -6947,22 +7185,22 @@ A SimpleX kiszolgálók nem látjhatják profilját. invited - meghívott + meghívta No comment provided by engineer. invited %@ - %@ meghívott + meghívta %@-t rcv group event chat item invited to connect - meghívott, hogy csatlakozzon + meghívta, hogy csatlakozzon chat list item title invited via your group link - meghívott a csoport hivatkozásán keresztül + meghívta a csoport hivatkozásán keresztül rcv group event chat item @@ -6972,12 +7210,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. join as %@ - csatlakozás mint %@ + csatlakozás mint: %@ No comment provided by engineer. left - elhagyta + elhagyta a csoportot rcv group event chat item @@ -6992,12 +7230,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. member %1$@ changed to %2$@ - %1$@ tag megváltoztatta a nevét erre: %2$@ + %1$@ megváltoztatta a nevét erre: %2$@ profile update event chat item connected - kapcsolódva + kapcsolódott rcv group event chat item @@ -7022,7 +7260,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. moderated by %@ - %@ által moderálva + moderálva lett %@ által marked deleted chat item preview text @@ -7087,6 +7325,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. tulajdonos member role + + owners + tulajdonosok + feature role + peer-to-peer ponttól-pontig @@ -7094,6 +7337,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. quantum resistant e2e encryption + végpontok közötti kvantumrezisztens titkosítás chat item text @@ -7123,7 +7367,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. removed contact address - törölt csatlakozási cím + törölt kapcsolattartási azonosító profile update event chat item @@ -7136,6 +7380,16 @@ A SimpleX kiszolgálók nem látjhatják profilját. eltávolítottak rcv group event chat item + + saved + mentett + No comment provided by engineer. + + + saved from %@ + mentve innen: %@ + No comment provided by engineer. + sec mp @@ -7153,7 +7407,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. security code changed - biztonsági kód megváltozott + a biztonsági kód megváltozott chat item text @@ -7163,7 +7417,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. set new contact address - új kapcsolattartási cím beállítása + új kapcsolattartási azonosító beállítása profile update event chat item @@ -7173,6 +7427,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. standard end-to-end encryption + szabványos végpontok közötti titkosítás chat item text @@ -7227,7 +7482,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. via contact address link - ismerős azonosítójának hivatkozásán keresztül + kapcsolattartási azonosító-hivatkozáson keresztül chat list item description @@ -7275,6 +7530,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. igen pref value + + you + ön + No comment provided by engineer. + you are invited to group meghívást kapott a csoportba @@ -7307,12 +7567,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. you changed role of %1$@ to %2$@ - megváltoztatta %1$@ szerepkörét erre: %@ + %1$@ szerepkörét megváltoztatta erre: %@ snd group event chat item you left - elhagyta + elhagyta a csoportot snd group event chat item @@ -7359,7 +7619,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - A SimpleX-nek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy csatlakozhasson más felhasználókhoz és videohívásokhoz. + A SimpleX-nek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz. Privacy - Camera Usage Description diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 34d39c405d..a5fe0ec830 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -104,7 +104,7 @@ %@ connected - %@ si è connesso/a + %@ connesso/a No comment provided by engineer. @@ -743,6 +743,11 @@ Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore) No comment provided by engineer. + + Allow to send SimpleX links. + Consenti di inviare link di SimpleX. + No comment provided by engineer. + Allow to send files and media. Consenti l'invio di file e contenuti multimediali. @@ -1083,6 +1088,11 @@ Impossibile ricevere il file No comment provided by engineer. + + Cellular + Mobile + No comment provided by engineer. + Change Cambia @@ -2078,6 +2088,11 @@ Non è reversibile! Esegui downgrade e apri chat No comment provided by engineer. + + Download + Scarica + chat item action + Download failed Scaricamento fallito @@ -2188,6 +2203,11 @@ Non è reversibile! Attiva il codice di autodistruzione set passcode view + + Enabled for + Attivo per + No comment provided by engineer. + Encrypt Crittografare @@ -2338,11 +2358,6 @@ Non è reversibile! Errore di aggiunta membro/i No comment provided by engineer. - - Error allowing contact PQ encryption - Errore nel consentire la crittografia PQ al contatto - No comment provided by engineer. - Error changing address Errore nella modifica dell'indirizzo @@ -2713,6 +2728,11 @@ Non è reversibile! File e contenuti multimediali sono vietati in questo gruppo. No comment provided by engineer. + + Files and media not allowed + File e multimediali non consentiti + No comment provided by engineer. + Files and media prohibited! File e contenuti multimediali vietati! @@ -2778,6 +2798,26 @@ Non è reversibile! Per console No comment provided by engineer. + + Forward + Inoltra + chat item action + + + Forward and save messages + Inoltra e salva i messaggi + No comment provided by engineer. + + + Forwarded + Inoltrato + No comment provided by engineer. + + + Forwarded from + Inoltrato da + No comment provided by engineer. + Found desktop Desktop trovato @@ -2888,6 +2928,11 @@ Non è reversibile! I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) No comment provided by engineer. + + Group members can send SimpleX links. + I membri del gruppo possono inviare link di Simplex. + No comment provided by engineer. + Group members can send direct messages. I membri del gruppo possono inviare messaggi diretti. @@ -3128,6 +3173,11 @@ Non è reversibile! In risposta a No comment provided by engineer. + + In-call sounds + Suoni nelle chiamate + No comment provided by engineer. + Incognito Incognito @@ -3603,6 +3653,11 @@ Questo è il tuo link per il gruppo %@! Le reazioni ai messaggi sono vietate in questo gruppo. No comment provided by engineer. + + Message source remains private. + La fonte del messaggio resta privata. + No comment provided by engineer. + Message text Testo del messaggio @@ -3718,6 +3773,11 @@ Questo è il tuo link per il gruppo %@! Altri miglioramenti sono in arrivo! No comment provided by engineer. + + More reliable network connection. + Connessione di rete più affidabile. + No comment provided by engineer. + Most likely this connection is deleted. Probabilmente questa connessione è stata eliminata. @@ -3753,6 +3813,16 @@ Questo è il tuo link per il gruppo %@! Rete e server No comment provided by engineer. + + Network connection + Connessione di rete + No comment provided by engineer. + + + Network management + Gestione della rete + No comment provided by engineer. + Network settings Impostazioni di rete @@ -3863,6 +3933,11 @@ Questo è il tuo link per il gruppo %@! Nessuna cronologia No comment provided by engineer. + + No network connection + Nessuna connessione di rete + No comment provided by engineer. + No permission to record voice message Nessuna autorizzazione per registrare messaggi vocali @@ -4077,6 +4152,11 @@ Questo è il tuo link per il gruppo %@! O mostra questo codice No comment provided by engineer. + + Other + Altro + No comment provided by engineer. + PING count Conteggio PING @@ -4244,11 +4324,6 @@ Errore: %@ Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata server test error - - Post-quantum E2EE - E2EE post-quantistica - No comment provided by engineer. - Preserve the last message draft, with attachments. Conserva la bozza dell'ultimo messaggio, con gli allegati. @@ -4299,6 +4374,11 @@ Errore: %@ Immagine del profilo No comment provided by engineer. + + Profile images + Immagini del profilo + No comment provided by engineer. + Profile name Nome del profilo @@ -4339,6 +4419,11 @@ Errore: %@ Proibisci le reazioni ai messaggi. No comment provided by engineer. + + Prohibit sending SimpleX links. + Vieta l'invio di link di SimpleX. + No comment provided by engineer. + Prohibit sending direct messages to members. Proibisci l'invio di messaggi diretti ai membri. @@ -4469,6 +4554,11 @@ Errore: %@ L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea. No comment provided by engineer. + + Receiving concurrency + Ricezione concomitanza + No comment provided by engineer. + Receiving file will be stopped. La ricezione del file verrà interrotta. @@ -4484,6 +4574,11 @@ Errore: %@ Cronologia recente e [bot della directory](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) migliorato. No comment provided by engineer. + + Recipient(s) can't see who this message is from. + I destinatari non possono vedere da chi proviene questo messaggio. + No comment provided by engineer. + Recipients see updates as you type them. I destinatari vedono gli aggiornamenti mentre li digiti. @@ -4784,11 +4879,21 @@ Errore: %@ Salvare il messaggio di benvenuto? No comment provided by engineer. + + Saved + Salvato + No comment provided by engineer. + Saved WebRTC ICE servers will be removed I server WebRTC ICE salvati verranno rimossi No comment provided by engineer. + + Saved from + Salvato da + No comment provided by engineer. + Saved message Messaggio salvato @@ -5094,6 +5199,11 @@ Errore: %@ Impostazioni No comment provided by engineer. + + Shape profile images + Forma delle immagini del profilo + No comment provided by engineer. + Share Condividi @@ -5212,6 +5322,16 @@ Errore: %@ SimpleX links Link di SimpleX + chat feature + + + SimpleX links are prohibited in this group. + I link di SimpleX sono vietati in questo gruppo. + No comment provided by engineer. + + + SimpleX links not allowed + Link di SimpleX non consentiti No comment provided by engineer. @@ -5249,6 +5369,11 @@ Errore: %@ Qualcuno notification title + + Square, circle, or anything in between. + Quadrata, circolare o qualsiasi forma tra le due. + No comment provided by engineer. + Start chat Avvia chat @@ -6050,6 +6175,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e I messaggi vocali sono vietati in questo gruppo. No comment provided by engineer. + + Voice messages not allowed + Messaggi vocali non consentiti + No comment provided by engineer. + Voice messages prohibited! Messaggi vocali vietati! @@ -6120,6 +6250,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Quando disponibili No comment provided by engineer. + + When connecting audio and video calls. + Quando si connettono le chiamate audio e video. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Quando le persone chiedono di connettersi, puoi accettare o rifiutare. @@ -6130,6 +6265,21 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Viene attivata nelle chat dirette! + No comment provided by engineer. + + + Wired ethernet + Cavo ethernet + No comment provided by engineer. + With encrypted files and media. Con file e multimediali criptati. @@ -6613,6 +6763,11 @@ I server di SimpleX non possono vedere il tuo profilo. amministratore member role + + admins + amministratori + feature role + agreeing encryption for %@… concordando la crittografia per %@… @@ -6623,6 +6778,11 @@ I server di SimpleX non possono vedere il tuo profilo. concordando la crittografia… chat item text + + all members + tutti i membri + feature role + always sempre @@ -6953,6 +7113,11 @@ I server di SimpleX non possono vedere il tuo profilo. evento accaduto No comment provided by engineer. + + forwarded + inoltrato + No comment provided by engineer. + group deleted gruppo eliminato @@ -7160,6 +7325,11 @@ I server di SimpleX non possono vedere il tuo profilo. proprietario member role + + owners + proprietari + feature role + peer-to-peer peer-to-peer @@ -7210,6 +7380,16 @@ I server di SimpleX non possono vedere il tuo profilo. ti ha rimosso/a rcv group event chat item + + saved + salvato + No comment provided by engineer. + + + saved from %@ + salvato da %@ + No comment provided by engineer. + sec sec @@ -7350,6 +7530,11 @@ I server di SimpleX non possono vedere il tuo profilo. sì pref value + + you + tu + No comment provided by engineer. + you are invited to group sei stato/a invitato/a al gruppo diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index e10c39bbc3..3f32998707 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -109,6 +109,7 @@ %@ downloaded + %@ ダウンロード済 No comment provided by engineer. @@ -133,6 +134,7 @@ %@ uploaded + %@ アップロード済 No comment provided by engineer. @@ -142,6 +144,7 @@ %@, %@ and %lld members + %@や%@など%lld人のメンバー No comment provided by engineer. @@ -211,6 +214,7 @@ %lld group events + %lld件のグループイベント No comment provided by engineer. @@ -220,10 +224,12 @@ %lld messages blocked + %lld件のメッセージをブロック No comment provided by engineer. %lld messages blocked by admin + %lldのメッセージが管理者によりブロック済 No comment provided by engineer. @@ -308,10 +314,12 @@ (new) + (新規) No comment provided by engineer. (this device v%@) + (このデバイス v%@) No comment provided by engineer. @@ -321,6 +329,7 @@ **Add contact**: to create a new invitation link, or connect via a link you received. + **コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。 No comment provided by engineer. @@ -330,6 +339,7 @@ **Create group**: to create a new group. + **グループ作成**: 新しいグループを作成する。 No comment provided by engineer. @@ -344,6 +354,7 @@ **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **注意**: 2つの端末で同じデータベースを使用すると、セキュリティ保護として、あなたが接続しているメッセージの復号化が解除されます。 No comment provided by engineer. @@ -363,16 +374,17 @@ **Warning**: the archive will be removed. + **警告**: アーカイブデータは削除されます。 No comment provided by engineer. **e2e encrypted** audio call - **e2e 暗号化**音声通話 + **エンドツーエンド暗号化済み**の音声通話 No comment provided by engineer. **e2e encrypted** video call - **e2e暗号化**ビデオ通話 + **エンドツーエンド暗号化済み**の テレビ電話 通話 No comment provided by engineer. @@ -422,6 +434,7 @@ 0 sec + 0 秒 time to disappear @@ -672,6 +685,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + すべての連絡先、会話、ファイルは安全に暗号化され、設定されたXFTPリレーに分割でアップロードされます。 No comment provided by engineer. @@ -716,7 +730,12 @@ Allow to irreversibly delete sent messages. (24 hours) - 送信済みメッセージの永久削除を許可する。 + 送信済みメッセージの永久削除を許可する。(24時間) + No comment provided by engineer. + + + Allow to send SimpleX links. + SimpleXリンクの送信を許可。 No comment provided by engineer. @@ -751,7 +770,7 @@ Allow your contacts to irreversibly delete sent messages. (24 hours) - 送信相手が永久メッセージ削除するのを許可する。 + 送信相手が永久メッセージ削除するのを許可する。(24時間) No comment provided by engineer. @@ -771,10 +790,12 @@ Already connecting! + 既に接続中です! No comment provided by engineer. Already joining the group! + すでにグループに参加しています! No comment provided by engineer. @@ -799,6 +820,7 @@ App data migration + アプリデータの移行 No comment provided by engineer. @@ -971,7 +993,7 @@ Both you and your contact can irreversibly delete sent messages. (24 hours) - あなたと連絡相手が送信済みメッセージを永久削除できます。 + あなたと連絡相手が送信済みメッセージを永久削除できます。(24時間) No comment provided by engineer. @@ -1042,6 +1064,10 @@ ファイル受信ができません No comment provided by engineer. + + Cellular + No comment provided by engineer. + Change 変更 @@ -1998,6 +2024,10 @@ This cannot be undone! ダウングレードしてチャットを開く No comment provided by engineer. + + Download + chat item action + Download failed No comment provided by engineer. @@ -2103,6 +2133,10 @@ This cannot be undone! 自己破壊パスコードを有効にする set passcode view + + Enabled for + No comment provided by engineer. + Encrypt 暗号化する @@ -2246,10 +2280,6 @@ This cannot be undone! メンバー追加にエラー発生 No comment provided by engineer. - - Error allowing contact PQ encryption - No comment provided by engineer. - Error changing address アドレス変更にエラー発生 @@ -2608,6 +2638,10 @@ This cannot be undone! このグループでは、ファイルとメディアは禁止されています。 No comment provided by engineer. + + Files and media not allowed + No comment provided by engineer. + Files and media prohibited! ファイルとメディアは禁止されています! @@ -2671,6 +2705,22 @@ This cannot be undone! コンソール No comment provided by engineer. + + Forward + chat item action + + + Forward and save messages + No comment provided by engineer. + + + Forwarded + No comment provided by engineer. + + + Forwarded from + No comment provided by engineer. + Found desktop No comment provided by engineer. @@ -2774,7 +2824,11 @@ This cannot be undone! Group members can irreversibly delete sent messages. (24 hours) - グループのメンバーがメッセージを完全削除することができます。 + グループのメンバーがメッセージを完全削除することができます。(24時間) + No comment provided by engineer. + + + Group members can send SimpleX links. No comment provided by engineer. @@ -3011,6 +3065,10 @@ This cannot be undone! 返信先 No comment provided by engineer. + + In-call sounds + No comment provided by engineer. + Incognito シークレットモード @@ -3466,6 +3524,10 @@ This is your link for group %@! このグループではメッセージへのリアクションは禁止されています。 No comment provided by engineer. + + Message source remains private. + No comment provided by engineer. + Message text メッセージ内容 @@ -3570,6 +3632,10 @@ This is your link for group %@! まだまだ改善してまいります! No comment provided by engineer. + + More reliable network connection. + No comment provided by engineer. + Most likely this connection is deleted. おそらく、この接続は削除されています。 @@ -3605,6 +3671,14 @@ This is your link for group %@! ネットワークとサーバ No comment provided by engineer. + + Network connection + No comment provided by engineer. + + + Network management + No comment provided by engineer. + Network settings ネットワーク設定 @@ -3714,6 +3788,10 @@ This is your link for group %@! 履歴はありません No comment provided by engineer. + + No network connection + No comment provided by engineer. + No permission to record voice message 音声メッセージを録音する権限がありません @@ -3818,7 +3896,7 @@ This is your link for group %@! Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours) - メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。 + メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。(24時間) No comment provided by engineer. @@ -3843,7 +3921,7 @@ This is your link for group %@! Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours) - メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。 + メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。(24時間) No comment provided by engineer. @@ -3919,6 +3997,10 @@ This is your link for group %@! Or show this code No comment provided by engineer. + + Other + No comment provided by engineer. + PING count PING回数 @@ -4078,10 +4160,6 @@ Error: %@ サーバアドレスの証明証IDが正しくないかもしれません server test error - - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. 添付を含めて、下書きを保存する。 @@ -4131,6 +4209,10 @@ Error: %@ プロフィール画像 No comment provided by engineer. + + Profile images + No comment provided by engineer. + Profile name No comment provided by engineer. @@ -4169,6 +4251,10 @@ Error: %@ メッセージへのリアクションは禁止されています。 No comment provided by engineer. + + Prohibit sending SimpleX links. + No comment provided by engineer. + Prohibit sending direct messages to members. メンバー間のダイレクトメッセージを使用禁止にする。 @@ -4295,6 +4381,10 @@ Error: %@ 開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。 No comment provided by engineer. + + Receiving concurrency + No comment provided by engineer. + Receiving file will be stopped. ファイルの受信を停止します。 @@ -4309,6 +4399,10 @@ Error: %@ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + No comment provided by engineer. + Recipients see updates as you type them. 受信者には、入力時に更新内容が表示されます。 @@ -4602,11 +4696,19 @@ Error: %@ ウェルカムメッセージを保存しますか? No comment provided by engineer. + + Saved + No comment provided by engineer. + Saved WebRTC ICE servers will be removed 保存されたWebRTC ICEサーバは削除されます No comment provided by engineer. + + Saved from + No comment provided by engineer. + Saved message message info title @@ -4897,6 +4999,10 @@ Error: %@ 設定 No comment provided by engineer. + + Shape profile images + No comment provided by engineer. + Share 共有する @@ -5013,6 +5119,14 @@ Error: %@ SimpleX links SimpleXリンク + chat feature + + + SimpleX links are prohibited in this group. + No comment provided by engineer. + + + SimpleX links not allowed No comment provided by engineer. @@ -5050,6 +5164,10 @@ Error: %@ 誰か notification title + + Square, circle, or anything in between. + No comment provided by engineer. + Start chat チャットを開始する @@ -5813,6 +5931,10 @@ To connect, please ask your contact to create another connection link and check このグループでは音声メッセージが使用禁止です。 No comment provided by engineer. + + Voice messages not allowed + No comment provided by engineer. + Voice messages prohibited! 音声メッセージは使用禁止です! @@ -5880,6 +6002,10 @@ To connect, please ask your contact to create another connection link and check 利用可能時に No comment provided by engineer. + + When connecting audio and video calls. + No comment provided by engineer. + When people request to connect, you can accept or reject it. 接続が要求されたら、それを受け入れるか拒否するかを選択できます。 @@ -5890,6 +6016,18 @@ To connect, please ask your contact to create another connection link and check 連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。 No comment provided by engineer. + + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + No comment provided by engineer. + + + Wired ethernet + No comment provided by engineer. + With encrypted files and media. No comment provided by engineer. @@ -6353,6 +6491,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 管理者 member role + + admins + feature role + agreeing encryption for %@… %@の暗号化に同意しています… @@ -6363,6 +6505,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 暗号化に同意しています… chat item text + + all members + feature role + always 常に @@ -6685,6 +6831,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 イベント発生 No comment provided by engineer. + + forwarded + No comment provided by engineer. + group deleted グループ削除済み @@ -6891,6 +7041,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 オーナー member role + + owners + feature role + peer-to-peer P2P @@ -6938,6 +7092,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 あなたを除名しました rcv group event chat item + + saved + No comment provided by engineer. + + + saved from %@ + No comment provided by engineer. + sec 秒 @@ -7070,6 +7232,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 はい pref value + + you + No comment provided by engineer. + you are invited to group グループ招待が届きました diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index d450d228d8..c5a9ba4ea8 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -743,6 +743,11 @@ Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur) No comment provided by engineer. + + Allow to send SimpleX links. + Sta toe dat SimpleX-links worden verzonden. + No comment provided by engineer. + Allow to send files and media. Sta toe om bestanden en media te verzenden. @@ -1083,6 +1088,11 @@ Kan bestand niet ontvangen No comment provided by engineer. + + Cellular + Mobiel + No comment provided by engineer. + Change Veranderen @@ -2078,6 +2088,11 @@ Dit kan niet ongedaan gemaakt worden! Downgraden en chat openen No comment provided by engineer. + + Download + Downloaden + chat item action + Download failed Download mislukt @@ -2188,6 +2203,11 @@ Dit kan niet ongedaan gemaakt worden! Zelfvernietigings wachtwoord inschakelen set passcode view + + Enabled for + Ingeschakeld voor + No comment provided by engineer. + Encrypt Versleutelen @@ -2338,11 +2358,6 @@ Dit kan niet ongedaan gemaakt worden! Fout bij het toevoegen van leden No comment provided by engineer. - - Error allowing contact PQ encryption - Fout bij het toestaan van contact PQ-versleuteling - No comment provided by engineer. - Error changing address Fout bij wijzigen van adres @@ -2713,6 +2728,11 @@ Dit kan niet ongedaan gemaakt worden! Bestanden en media zijn verboden in deze groep. No comment provided by engineer. + + Files and media not allowed + Bestanden en media niet toegestaan + No comment provided by engineer. + Files and media prohibited! Bestanden en media verboden! @@ -2778,6 +2798,26 @@ Dit kan niet ongedaan gemaakt worden! Voor console No comment provided by engineer. + + Forward + Doorsturen + chat item action + + + Forward and save messages + Berichten doorsturen en opslaan + No comment provided by engineer. + + + Forwarded + Doorgestuurd + No comment provided by engineer. + + + Forwarded from + Doorgestuurd vanuit + No comment provided by engineer. + Found desktop Desktop gevonden @@ -2888,6 +2928,11 @@ Dit kan niet ongedaan gemaakt worden! Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur) No comment provided by engineer. + + Group members can send SimpleX links. + Groepsleden kunnen SimpleX-links verzenden. + No comment provided by engineer. + Group members can send direct messages. Groepsleden kunnen directe berichten sturen. @@ -3128,6 +3173,11 @@ Dit kan niet ongedaan gemaakt worden! In antwoord op No comment provided by engineer. + + In-call sounds + Geluiden tijdens het bellen + No comment provided by engineer. + Incognito Incognito @@ -3603,6 +3653,11 @@ Dit is jouw link voor groep %@! Reacties op berichten zijn verboden in deze groep. No comment provided by engineer. + + Message source remains private. + Berichtbron blijft privé. + No comment provided by engineer. + Message text Bericht tekst @@ -3718,6 +3773,11 @@ Dit is jouw link voor groep %@! Meer verbeteringen volgen snel! No comment provided by engineer. + + More reliable network connection. + Betrouwbaardere netwerkverbinding. + No comment provided by engineer. + Most likely this connection is deleted. Hoogstwaarschijnlijk is deze verbinding verwijderd. @@ -3753,6 +3813,16 @@ Dit is jouw link voor groep %@! Netwerk & servers No comment provided by engineer. + + Network connection + Netwerkverbinding + No comment provided by engineer. + + + Network management + Netwerkbeheer + No comment provided by engineer. + Network settings Netwerk instellingen @@ -3863,6 +3933,11 @@ Dit is jouw link voor groep %@! Geen geschiedenis No comment provided by engineer. + + No network connection + Geen netwerkverbinding + No comment provided by engineer. + No permission to record voice message Geen toestemming om spraakbericht op te nemen @@ -4077,6 +4152,11 @@ Dit is jouw link voor groep %@! Of laat deze code zien No comment provided by engineer. + + Other + Ander + No comment provided by engineer. + PING count PING count @@ -4244,11 +4324,6 @@ Fout: %@ Mogelijk is de certificaat vingerafdruk in het server adres onjuist server test error - - Post-quantum E2EE - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Bewaar het laatste berichtconcept, met bijlagen. @@ -4299,6 +4374,11 @@ Fout: %@ profielfoto No comment provided by engineer. + + Profile images + Profiel afbeeldingen + No comment provided by engineer. + Profile name Profielnaam @@ -4339,6 +4419,11 @@ Fout: %@ Berichten reacties verbieden. No comment provided by engineer. + + Prohibit sending SimpleX links. + Verbied het verzenden van SimpleX-links + No comment provided by engineer. + Prohibit sending direct messages to members. Verbied het sturen van directe berichten naar leden. @@ -4469,6 +4554,11 @@ Fout: %@ Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is. No comment provided by engineer. + + Receiving concurrency + Gelijktijdig ontvangen + No comment provided by engineer. + Receiving file will be stopped. Het ontvangen van het bestand wordt gestopt. @@ -4484,6 +4574,11 @@ Fout: %@ Recente geschiedenis en verbeterde [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Ontvanger(s) kunnen niet zien van wie dit bericht afkomstig is. + No comment provided by engineer. + Recipients see updates as you type them. Ontvangers zien updates terwijl u ze typt. @@ -4784,11 +4879,21 @@ Fout: %@ Welkomst bericht opslaan? No comment provided by engineer. + + Saved + Opgeslagen + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Opgeslagen WebRTC ICE servers worden verwijderd No comment provided by engineer. + + Saved from + Opgeslagen van + No comment provided by engineer. + Saved message Opgeslagen bericht @@ -5094,6 +5199,11 @@ Fout: %@ Instellingen No comment provided by engineer. + + Shape profile images + Vorm profiel afbeeldingen + No comment provided by engineer. + Share Deel @@ -5212,6 +5322,16 @@ Fout: %@ SimpleX links SimpleX links + chat feature + + + SimpleX links are prohibited in this group. + SimpleX-links zijn in deze groep verboden. + No comment provided by engineer. + + + SimpleX links not allowed + SimpleX-links zijn niet toegestaan No comment provided by engineer. @@ -5249,6 +5369,11 @@ Fout: %@ Iemand notification title + + Square, circle, or anything in between. + Vierkant, cirkel of iets daartussenin. + No comment provided by engineer. + Start chat Begin gesprek @@ -6050,6 +6175,11 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Spraak berichten zijn verboden in deze groep. No comment provided by engineer. + + Voice messages not allowed + Spraakberichten niet toegestaan + No comment provided by engineer. + Voice messages prohibited! Spraak berichten verboden! @@ -6120,6 +6250,11 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Wanneer beschikbaar No comment provided by engineer. + + When connecting audio and video calls. + Bij het verbinden van audio- en video-oproepen. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Wanneer mensen vragen om verbinding te maken, kunt u dit accepteren of weigeren. @@ -6130,6 +6265,21 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen. No comment provided by engineer. + + WiFi + Wifi + No comment provided by engineer. + + + Will be enabled in direct chats! + Wordt ingeschakeld in directe chats! + No comment provided by engineer. + + + Wired ethernet + Bekabeld Ethernet + No comment provided by engineer. + With encrypted files and media. ‐Met versleutelde bestanden en media. @@ -6613,6 +6763,11 @@ SimpleX servers kunnen uw profiel niet zien. Beheerder member role + + admins + beheerders + feature role + agreeing encryption for %@… versleuteling overeenkomen voor %@… @@ -6623,6 +6778,11 @@ SimpleX servers kunnen uw profiel niet zien. versleuteling overeenkomen… chat item text + + all members + alle leden + feature role + always altijd @@ -6953,6 +7113,11 @@ SimpleX servers kunnen uw profiel niet zien. gebeurtenis gebeurd No comment provided by engineer. + + forwarded + doorgestuurd + No comment provided by engineer. + group deleted groep verwijderd @@ -7160,6 +7325,11 @@ SimpleX servers kunnen uw profiel niet zien. Eigenaar member role + + owners + eigenaren + feature role + peer-to-peer peer-to-peer @@ -7210,6 +7380,16 @@ SimpleX servers kunnen uw profiel niet zien. heeft je verwijderd rcv group event chat item + + saved + opgeslagen + No comment provided by engineer. + + + saved from %@ + opgeslagen van %@ + No comment provided by engineer. + sec sec @@ -7350,6 +7530,11 @@ SimpleX servers kunnen uw profiel niet zien. Ja pref value + + you + jij + No comment provided by engineer. + you are invited to group je bent uitgenodigd voor de groep diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index f4d10b0dc0..d71bd5843a 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -39,7 +39,7 @@ !1 colored! - !1 kolorowy! + !1 pokolorowany! No comment provided by engineer. @@ -344,32 +344,32 @@ **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. - **Bardziej prywatny**: sprawdzanie nowych wiadomości co 20 minut. Token urządzenia jest współdzielony z serwerem SimpleX Chat, ale nie informacje o liczbie kontaktów lub wiadomości. + **Bardziej prywatny**: sprawdzanie nowych wiadomości odbywa się co 20 minut. Współdzielony z serwerem SimpleX Chat jest token urządzenia, lecz nie informacje o liczbie kontaktów lub wiadomości. No comment provided by engineer. **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). - **Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, sprawdzaj wiadomości okresowo w tle (zależy jak często korzystasz z aplikacji). + **Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, wiadomości sprawdzane są co jakiś czas w tle (zależne od tego jak często korzystasz z aplikacji). No comment provided by engineer. **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. - *Uwaga*: używanie tej samej bazy danych na dwóch urządzeniach zepsuje odszyfrowywanie wiadomości twoich połączeń, jako zabezpieczenie. + *Uwaga*: w celach bezpieczeństwa użycie tej samej bazy danych na dwóch różnych urządzeniach spowoduje brak możliwości odszyfrowywania wiadomości z Twoich połączeń. No comment provided by engineer. **Please note**: you will NOT be able to recover or change passphrase if you lose it. - **Uwaga**: NIE będziesz w stanie odzyskać lub zmienić hasła, jeśli je stracisz. + **Uwaga**: NIE będziesz w stanie odzyskać lub zmienić kodu dostępu, jeśli go stracisz. No comment provided by engineer. **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - **Zalecane**: token urządzenia i powiadomienia są wysyłane do serwera powiadomień SimpleX Chat, ale nie treść wiadomości, rozmiar lub od kogo jest. + **Zalecane**: do serwera powiadomień SimpleX Chat wysyłany jest token urządzenia i powiadomienia, lecz nie treść wiadomości, jej rozmiar lub od kogo ona jest. No comment provided by engineer. **Warning**: Instant push notifications require passphrase saved in Keychain. - **Uwaga**: Natychmiastowe powiadomienia push wymagają hasła zapisanego w Keychain. + **Uwaga**: Natychmiastowe powiadomienia push wymagają zapisania kodu dostępu w Keychain. No comment provided by engineer. @@ -743,6 +743,11 @@ Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny) No comment provided by engineer. + + Allow to send SimpleX links. + Zezwól na wysyłanie linków SimpleX. + No comment provided by engineer. + Allow to send files and media. Pozwól na wysyłanie plików i mediów. @@ -1083,6 +1088,11 @@ Nie można odebrać pliku No comment provided by engineer. + + Cellular + Sieć komórkowa + No comment provided by engineer. + Change Zmień @@ -2078,6 +2088,11 @@ To nie może być cofnięte! Obniż wersję i otwórz czat No comment provided by engineer. + + Download + Pobierz + chat item action + Download failed Pobieranie nie udane @@ -2188,6 +2203,11 @@ To nie może być cofnięte! Włącz pin samodestrukcji set passcode view + + Enabled for + Włączony dla + No comment provided by engineer. + Encrypt Szyfruj @@ -2338,11 +2358,6 @@ To nie może być cofnięte! Błąd dodawania członka(ów) No comment provided by engineer. - - Error allowing contact PQ encryption - Błąd pozwalania kontaktowi na szyfrowanie PQ - No comment provided by engineer. - Error changing address Błąd zmiany adresu @@ -2713,6 +2728,11 @@ To nie może być cofnięte! Pliki i media są zabronione w tej grupie. No comment provided by engineer. + + Files and media not allowed + Pliki i multimedia nie są dozwolone + No comment provided by engineer. + Files and media prohibited! Pliki i media zabronione! @@ -2778,6 +2798,26 @@ To nie może być cofnięte! Dla konsoli No comment provided by engineer. + + Forward + Przekaż dalej + chat item action + + + Forward and save messages + Przesyłaj dalej i zapisuj wiadomości + No comment provided by engineer. + + + Forwarded + Przekazane dalej + No comment provided by engineer. + + + Forwarded from + Przekazane dalej od + No comment provided by engineer. + Found desktop Znaleziono komputer @@ -2888,6 +2928,11 @@ To nie może być cofnięte! Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) No comment provided by engineer. + + Group members can send SimpleX links. + Członkowie grupy mogą wysyłać linki SimpleX. + No comment provided by engineer. + Group members can send direct messages. Członkowie grupy mogą wysyłać bezpośrednie wiadomości. @@ -3128,6 +3173,11 @@ To nie może być cofnięte! W odpowiedzi na No comment provided by engineer. + + In-call sounds + Dźwięki w rozmowie + No comment provided by engineer. + Incognito Incognito @@ -3603,6 +3653,11 @@ To jest twój link do grupy %@! Reakcje wiadomości są zabronione w tej grupie. No comment provided by engineer. + + Message source remains private. + Źródło wiadomości pozostaje prywatne. + No comment provided by engineer. + Message text Tekst wiadomości @@ -3718,6 +3773,11 @@ To jest twój link do grupy %@! Więcej ulepszeń już wkrótce! No comment provided by engineer. + + More reliable network connection. + Bardziej niezawodne połączenia sieciowe. + No comment provided by engineer. + Most likely this connection is deleted. Najprawdopodobniej to połączenie jest usunięte. @@ -3753,6 +3813,16 @@ To jest twój link do grupy %@! Sieć i serwery No comment provided by engineer. + + Network connection + Połączenie z siecią + No comment provided by engineer. + + + Network management + Zarządzenie sieciowe + No comment provided by engineer. + Network settings Ustawienia sieci @@ -3863,6 +3933,11 @@ To jest twój link do grupy %@! Brak historii No comment provided by engineer. + + No network connection + Brak połączenia z siecią + No comment provided by engineer. + No permission to record voice message Brak uprawnień do nagrywania wiadomości głosowej @@ -4077,6 +4152,11 @@ To jest twój link do grupy %@! Lub pokaż ten kod No comment provided by engineer. + + Other + Inne + No comment provided by engineer. + PING count Liczba PINGÓW @@ -4201,12 +4281,12 @@ Błąd: %@ Please enter correct current passphrase. - Wprowadź poprawne aktualne hasło. + Wprowadź poprawny obecny kod dostępu. No comment provided by engineer. Please enter the previous password after restoring database backup. This action can not be undone. - Proszę podać poprzednie hasło po przywróceniu kopii zapasowej bazy danych. Tej czynności nie można cofnąć. + Proszę podać poprzedni kod dostępu po przywróceniu kopii zapasowej bazy danych. Tej czynności nie można cofnąć. No comment provided by engineer. @@ -4226,12 +4306,12 @@ Błąd: %@ Please store passphrase securely, you will NOT be able to access chat if you lose it. - Prosimy o bezpieczne przechowywanie hasła, w przypadku jego utraty NIE będzie można uzyskać dostępu do czatu. + Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można uzyskać dostępu do czatu. No comment provided by engineer. Please store passphrase securely, you will NOT be able to change it if you lose it. - Prosimy o bezpieczne przechowywanie hasła, w przypadku jego utraty NIE będzie można go zmienić. + Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można go zmienić. No comment provided by engineer. @@ -4244,11 +4324,6 @@ Błąd: %@ Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy server test error - - Post-quantum E2EE - Postkwantowe E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami. @@ -4299,6 +4374,11 @@ Błąd: %@ Zdjęcie profilowe No comment provided by engineer. + + Profile images + Zdjęcia profilowe + No comment provided by engineer. + Profile name Nazwa profilu @@ -4339,6 +4419,11 @@ Błąd: %@ Zabroń reakcje wiadomości. No comment provided by engineer. + + Prohibit sending SimpleX links. + Zabroń wysyłania linków SimpleX. + No comment provided by engineer. + Prohibit sending direct messages to members. Zabroń wysyłania bezpośrednich wiadomości do członków. @@ -4469,6 +4554,11 @@ Błąd: %@ Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online. No comment provided by engineer. + + Receiving concurrency + Konkurencyjne odbieranie + No comment provided by engineer. + Receiving file will be stopped. Odbieranie pliku zostanie przerwane. @@ -4484,6 +4574,11 @@ Błąd: %@ Ostania historia i ulepszony [bot adresowy](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Odbiorca/y nie mogą zobaczyć od kogo jest ta wiadomość. + No comment provided by engineer. + Recipients see updates as you type them. Odbiorcy widzą aktualizacje podczas ich wpisywania. @@ -4784,11 +4879,21 @@ Błąd: %@ Zapisać wiadomość powitalną? No comment provided by engineer. + + Saved + Zapisane + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Zapisane serwery WebRTC ICE zostaną usunięte No comment provided by engineer. + + Saved from + Zapisane od + No comment provided by engineer. + Saved message Zachowano wiadomość @@ -5094,6 +5199,11 @@ Błąd: %@ Ustawienia No comment provided by engineer. + + Shape profile images + Kształtuj obrazy profilowe + No comment provided by engineer. + Share Udostępnij @@ -5212,6 +5322,16 @@ Błąd: %@ SimpleX links Linki SimpleX + chat feature + + + SimpleX links are prohibited in this group. + Linki SimpleX są zablokowane na tej grupie. + No comment provided by engineer. + + + SimpleX links not allowed + Linki SimpleX są niedozwolone No comment provided by engineer. @@ -5249,6 +5369,11 @@ Błąd: %@ Ktoś notification title + + Square, circle, or anything in between. + Kwadrat, okrąg lub cokolwiek pomiędzy. + No comment provided by engineer. + Start chat Rozpocznij czat @@ -6050,6 +6175,11 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wiadomości głosowe są zabronione w tej grupie. No comment provided by engineer. + + Voice messages not allowed + Wiadomości głosowe są niedozwolone + No comment provided by engineer. + Voice messages prohibited! Wiadomości głosowe zabronione! @@ -6120,6 +6250,11 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Gdy dostępny No comment provided by engineer. + + When connecting audio and video calls. + Podczas łączenia połączeń audio i wideo. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić. @@ -6130,6 +6265,21 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Zostanie włączone w czatach bezpośrednich! + No comment provided by engineer. + + + Wired ethernet + Połączenie ethernet (po kablu) + No comment provided by engineer. + With encrypted files and media. Z zaszyfrowanymi plikami i multimediami. @@ -6613,6 +6763,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. administrator member role + + admins + administratorzy + feature role + agreeing encryption for %@… uzgadnianie szyfrowania dla %@… @@ -6623,6 +6778,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. uzgadnianie szyfrowania… chat item text + + all members + wszyscy członkowie + feature role + always zawsze @@ -6953,6 +7113,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. nowe wydarzenie No comment provided by engineer. + + forwarded + przekazane dalej + No comment provided by engineer. + group deleted grupa usunięta @@ -7160,6 +7325,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. właściciel member role + + owners + właściciele + feature role + peer-to-peer peer-to-peer @@ -7210,6 +7380,16 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. usunął cię rcv group event chat item + + saved + zapisane + No comment provided by engineer. + + + saved from %@ + zapisane od %@ + No comment provided by engineer. + sec sek @@ -7350,6 +7530,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. tak pref value + + you + Ty + No comment provided by engineer. + you are invited to group jesteś zaproszony do grupy diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index a31d4e314d..c9c9707c39 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -610,160 +610,197 @@ Available in v5.1 Can't delete user profile! No comment provided by engineer. - + Can't invite contact! + Não é possível convidar contato! No comment provided by engineer. - + Can't invite contacts! + Não é possível convidar contatos! No comment provided by engineer. - + Cancel + Cancelar No comment provided by engineer. - + Cannot access keychain to save database password + Não é possível acessar as chaves para salvar a senha do banco de dados No comment provided by engineer. - + Cannot receive file + Não é possível receber o arquivo No comment provided by engineer. - + Change + Mudar No comment provided by engineer. Change passcode No comment provided by engineer. - + Change database passphrase? + Alterar a senha do banco de dados? No comment provided by engineer. - + Change lock mode + Alterar modo de bloqueio authentication reason - + Change member role? + Alterar a função do membro? No comment provided by engineer. - + Change passcode + Alterar senha authentication reason - + Change receiving address + Alterar endereço de recebimento No comment provided by engineer. - + Change receiving address? + Alterar endereço de recebimento? No comment provided by engineer. - + Change role + Mudar função No comment provided by engineer. - + Chat archive + Arquivar conversa No comment provided by engineer. - + Chat console + Console da conversa No comment provided by engineer. - + Chat database + Banco de Dados da conversa No comment provided by engineer. - + Chat database deleted + Banco de Dados da conversa apagado No comment provided by engineer. - + Chat database imported + Banco de Dados da conversa importado No comment provided by engineer. - + Chat is running + A conversa está em execução No comment provided by engineer. Chat is stopped No comment provided by engineer. - + Chat preferences + Preferências da conversa No comment provided by engineer. - + Chats + Conversas No comment provided by engineer. - + Check server address and try again. + Verifique o endereço do servidor e tente novamente. No comment provided by engineer. - + Chinese and Spanish interface + Interface em Chinês e Espanhol No comment provided by engineer. - + Choose file + Escolher arquivo No comment provided by engineer. - + Choose from library + Escolha da biblioteca No comment provided by engineer. - + Clear + Limpar No comment provided by engineer. - + Clear conversation + Limpar conversa No comment provided by engineer. - + Clear conversation? + Limpar conversa? No comment provided by engineer. - + Clear verification + Limpar verificação No comment provided by engineer. - + Colors + Cores No comment provided by engineer. - + Compare file + Comparar arquivo server test step - + Compare security codes with your contacts. + Compare os códigos de segurança com seus contatos. No comment provided by engineer. - + Configure ICE servers + Configurar servidores ICE No comment provided by engineer. - + Confirm + Confirme No comment provided by engineer. - + Confirm Passcode + Confirmar senha No comment provided by engineer. - + Confirm database upgrades + Confirmar atualizações do banco de dados No comment provided by engineer. - + Confirm new passphrase… + Confirme a nova senha… No comment provided by engineer. @@ -4447,6 +4484,62 @@ SimpleX servers cannot see your profile. Permitir apagar irreversivelmente as mensagens enviadas. (24 horas) No comment provided by engineer. + + Choose _Migrate from another device_ on the new device and scan QR code. + Escolha _Migrar de outro dispositivo_ no novo dispositivo e leia o código QR. + No comment provided by engineer. + + + Cancel migration + Cancelar migração + No comment provided by engineer. + + + Camera not available + Câmera não disponível + No comment provided by engineer. + + + Change self-destruct passcode + Alterar a senha de autodestruição + authentication reason + set passcode view + + + Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. + A conversa foi interrompida. Se você já usou esse banco de dados em outro dispositivo, deverá transferi-lo de volta antes de iniciar a conversa. + No comment provided by engineer. + + + Chat migrated! + Conversa migrada! + No comment provided by engineer. + + + Clear private notes? + Limpar notas privadas? + No comment provided by engineer. + + + Confirm network settings + Confirme as configurações de rede + No comment provided by engineer. + + + Confirm that you remember database passphrase to migrate it. + Confirme se você se lembra da senha do banco de dados para migrá-lo. + No comment provided by engineer. + + + Change self-destruct mode + Alterar modo de autodestruição + authentication reason + + + Confirm upload + Confirmar envio + No comment provided by engineer. + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 7d5f119b9d..ba1ac7a929 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -743,6 +743,11 @@ Разрешить необратимо удалять отправленные сообщения. (24 часа) No comment provided by engineer. + + Allow to send SimpleX links. + Разрешить отправлять ссылки SimpleX. + No comment provided by engineer. + Allow to send files and media. Разрешить посылать файлы и медиа. @@ -1083,6 +1088,11 @@ Невозможно получить файл No comment provided by engineer. + + Cellular + Мобильная сеть + No comment provided by engineer. + Change Поменять @@ -2078,6 +2088,11 @@ This cannot be undone! Откатить версию и открыть чат No comment provided by engineer. + + Download + Загрузить + chat item action + Download failed Ошибка загрузки @@ -2188,6 +2203,11 @@ This cannot be undone! Включить код самоуничтожения set passcode view + + Enabled for + Включено для + No comment provided by engineer. + Encrypt Зашифровать @@ -2338,11 +2358,6 @@ This cannot be undone! Ошибка при добавлении членов группы No comment provided by engineer. - - Error allowing contact PQ encryption - Ошибка разрешения квантово-устойчивого шифрования - No comment provided by engineer. - Error changing address Ошибка при изменении адреса @@ -2713,6 +2728,11 @@ This cannot be undone! Файлы и медиа запрещены в этой группе. No comment provided by engineer. + + Files and media not allowed + Файлы и медиа не разрешены + No comment provided by engineer. + Files and media prohibited! Файлы и медиа запрещены! @@ -2778,6 +2798,26 @@ This cannot be undone! Для консоли No comment provided by engineer. + + Forward + Переслать + chat item action + + + Forward and save messages + Переслать и сохранить сообщение + No comment provided by engineer. + + + Forwarded + Переслано + No comment provided by engineer. + + + Forwarded from + Переслано из + No comment provided by engineer. + Found desktop Компьютер найден @@ -2850,7 +2890,7 @@ This cannot be undone! Group image - Аватар группы + Картинка группы No comment provided by engineer. @@ -2888,6 +2928,11 @@ This cannot be undone! Члены группы могут необратимо удалять отправленные сообщения. (24 часа) No comment provided by engineer. + + Group members can send SimpleX links. + Члены группы могут отправлять ссылки SimpleX. + No comment provided by engineer. + Group members can send direct messages. Члены группы могут посылать прямые сообщения. @@ -3128,6 +3173,11 @@ This cannot be undone! В ответ на No comment provided by engineer. + + In-call sounds + Звуки во время звонков + No comment provided by engineer. + Incognito Инкогнито @@ -3603,6 +3653,11 @@ This is your link for group %@! Реакции на сообщения запрещены в этой группе. No comment provided by engineer. + + Message source remains private. + Источник сообщения остаётся конфиденциальным. + No comment provided by engineer. + Message text Текст сообщения @@ -3718,6 +3773,11 @@ This is your link for group %@! Дополнительные улучшения скоро! No comment provided by engineer. + + More reliable network connection. + Более надежное соединение с сетью. + No comment provided by engineer. + Most likely this connection is deleted. Скорее всего, соединение удалено. @@ -3753,6 +3813,16 @@ This is your link for group %@! Сеть & серверы No comment provided by engineer. + + Network connection + Интернет-соединение + No comment provided by engineer. + + + Network management + Статус сети + No comment provided by engineer. + Network settings Настройки сети @@ -3863,6 +3933,11 @@ This is your link for group %@! Нет истории No comment provided by engineer. + + No network connection + Нет интернет-соединения + No comment provided by engineer. + No permission to record voice message Нет разрешения для записи голосового сообщения @@ -4077,6 +4152,11 @@ This is your link for group %@! Или покажите этот код No comment provided by engineer. + + Other + Другaя сеть + No comment provided by engineer. + PING count Количество PING @@ -4244,11 +4324,6 @@ Error: %@ Возможно, хэш сертификата в адресе сервера неверный server test error - - Post-quantum E2EE - Квантово-устойчивое E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Сохранить последний черновик, вместе с вложениями. @@ -4296,7 +4371,12 @@ Error: %@ Profile image - Аватар + Картинка профиля + No comment provided by engineer. + + + Profile images + Картинки профилей No comment provided by engineer. @@ -4339,6 +4419,11 @@ Error: %@ Запретить реакции на сообщения. No comment provided by engineer. + + Prohibit sending SimpleX links. + Запретить отправку ссылок SimpleX. + No comment provided by engineer. + Prohibit sending direct messages to members. Запретить посылать прямые сообщения членам группы. @@ -4469,6 +4554,11 @@ Error: %@ Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн. No comment provided by engineer. + + Receiving concurrency + Одновременный приём + No comment provided by engineer. + Receiving file will be stopped. Приём файла будет прекращён. @@ -4484,6 +4574,11 @@ Error: %@ История сообщений и улучшенный [каталог групп](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Получатели не видят от кого это сообщение. + No comment provided by engineer. + Recipients see updates as you type them. Получатели видят их в то время как Вы их набираете. @@ -4784,11 +4879,21 @@ Error: %@ Сохранить приветственное сообщение? No comment provided by engineer. + + Saved + Сохранено + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Сохраненные WebRTC ICE серверы будут удалены No comment provided by engineer. + + Saved from + Сохранено из + No comment provided by engineer. + Saved message Сохраненное сообщение @@ -5094,6 +5199,11 @@ Error: %@ Настройки No comment provided by engineer. + + Shape profile images + Форма картинок профилей + No comment provided by engineer. + Share Поделиться @@ -5212,6 +5322,16 @@ Error: %@ SimpleX links SimpleX ссылки + chat feature + + + SimpleX links are prohibited in this group. + Ссылки SimpleX запрещены в этой группе. + No comment provided by engineer. + + + SimpleX links not allowed + Ссылки SimpleX не разрешены No comment provided by engineer. @@ -5249,6 +5369,11 @@ Error: %@ Контакт notification title + + Square, circle, or anything in between. + Квадрат, круг и все, что между ними. + No comment provided by engineer. + Start chat Запустить чат @@ -6050,6 +6175,11 @@ To connect, please ask your contact to create another connection link and check Голосовые сообщения запрещены в этой группе. No comment provided by engineer. + + Voice messages not allowed + Голосовые сообщения не разрешены + No comment provided by engineer. + Voice messages prohibited! Голосовые сообщения запрещены! @@ -6120,6 +6250,11 @@ To connect, please ask your contact to create another connection link and check Когда возможно No comment provided by engineer. + + When connecting audio and video calls. + Во время соединения аудио и видео звонков. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Когда Вы получите запрос на соединение, Вы можете принять или отклонить его. @@ -6130,6 +6265,21 @@ To connect, please ask your contact to create another connection link and check Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Будет включено в прямых разговорах! + No comment provided by engineer. + + + Wired ethernet + Проводная сеть + No comment provided by engineer. + With encrypted files and media. С зашифрованными файлами и медиа. @@ -6613,6 +6763,11 @@ SimpleX серверы не могут получить доступ к Ваше админ member role + + admins + админы + feature role + agreeing encryption for %@… шифрование согласовывается для %@… @@ -6623,6 +6778,11 @@ SimpleX серверы не могут получить доступ к Ваше шифрование согласовывается… chat item text + + all members + все члены + feature role + always всегда @@ -6953,6 +7113,11 @@ SimpleX серверы не могут получить доступ к Ваше событие произошло No comment provided by engineer. + + forwarded + переслано + No comment provided by engineer. + group deleted группа удалена @@ -7160,6 +7325,11 @@ SimpleX серверы не могут получить доступ к Ваше владелец member role + + owners + владельцы + feature role + peer-to-peer peer-to-peer @@ -7210,6 +7380,16 @@ SimpleX серверы не могут получить доступ к Ваше удалил(а) Вас из группы rcv group event chat item + + saved + сохранено + No comment provided by engineer. + + + saved from %@ + сохранено из %@ + No comment provided by engineer. + sec сек @@ -7350,6 +7530,11 @@ SimpleX серверы не могут получить доступ к Ваше да pref value + + you + Вы + No comment provided by engineer. + you are invited to group Вы приглашены в группу diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index e1b0e4d3e4..1f62fad60f 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -707,6 +707,10 @@ อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร No comment provided by engineer. + + Allow to send SimpleX links. + No comment provided by engineer. + Allow to send files and media. อนุญาตให้ส่งไฟล์และสื่อ @@ -1028,6 +1032,10 @@ ไม่สามารถรับไฟล์ได้ No comment provided by engineer. + + Cellular + No comment provided by engineer. + Change เปลี่ยน @@ -1979,6 +1987,10 @@ This cannot be undone! ปรับลดรุ่นและเปิดแชท No comment provided by engineer. + + Download + chat item action + Download failed No comment provided by engineer. @@ -2084,6 +2096,10 @@ This cannot be undone! เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง set passcode view + + Enabled for + No comment provided by engineer. + Encrypt Encrypt @@ -2225,10 +2241,6 @@ This cannot be undone! เกิดข้อผิดพลาดในการเพิ่มสมาชิก No comment provided by engineer. - - Error allowing contact PQ encryption - No comment provided by engineer. - Error changing address เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่ @@ -2586,6 +2598,10 @@ This cannot be undone! ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. + + Files and media not allowed + No comment provided by engineer. + Files and media prohibited! ไฟล์และสื่อต้องห้าม! @@ -2649,6 +2665,22 @@ This cannot be undone! สำหรับคอนโซล No comment provided by engineer. + + Forward + chat item action + + + Forward and save messages + No comment provided by engineer. + + + Forwarded + No comment provided by engineer. + + + Forwarded from + No comment provided by engineer. + Found desktop No comment provided by engineer. @@ -2755,6 +2787,10 @@ This cannot be undone! สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร No comment provided by engineer. + + Group members can send SimpleX links. + No comment provided by engineer. + Group members can send direct messages. สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้ @@ -2989,6 +3025,10 @@ This cannot be undone! ในการตอบกลับถึง No comment provided by engineer. + + In-call sounds + No comment provided by engineer. + Incognito ไม่ระบุตัวตน @@ -3443,6 +3483,10 @@ This is your link for group %@! ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. + + Message source remains private. + No comment provided by engineer. + Message text ข้อความ @@ -3547,6 +3591,10 @@ This is your link for group %@! การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้! No comment provided by engineer. + + More reliable network connection. + No comment provided by engineer. + Most likely this connection is deleted. item status description @@ -3581,6 +3629,14 @@ This is your link for group %@! เครือข่ายและเซิร์ฟเวอร์ No comment provided by engineer. + + Network connection + No comment provided by engineer. + + + Network management + No comment provided by engineer. + Network settings การตั้งค่าเครือข่าย @@ -3688,6 +3744,10 @@ This is your link for group %@! ไม่มีประวัติ No comment provided by engineer. + + No network connection + No comment provided by engineer. + No permission to record voice message ไม่อนุญาตให้บันทึกข้อความเสียง @@ -3892,6 +3952,10 @@ This is your link for group %@! Or show this code No comment provided by engineer. + + Other + No comment provided by engineer. + PING count จํานวน PING @@ -4051,10 +4115,6 @@ Error: %@ อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง server test error - - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ @@ -4104,6 +4164,10 @@ Error: %@ รูปโปรไฟล์ No comment provided by engineer. + + Profile images + No comment provided by engineer. + Profile name No comment provided by engineer. @@ -4142,6 +4206,10 @@ Error: %@ ห้ามแสดงปฏิกิริยาต่อข้อความ No comment provided by engineer. + + Prohibit sending SimpleX links. + No comment provided by engineer. + Prohibit sending direct messages to members. ห้ามส่งข้อความโดยตรงถึงสมาชิก @@ -4268,6 +4336,10 @@ Error: %@ ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์ No comment provided by engineer. + + Receiving concurrency + No comment provided by engineer. + Receiving file will be stopped. การรับไฟล์จะหยุดลง @@ -4282,6 +4354,10 @@ Error: %@ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + No comment provided by engineer. + Recipients see updates as you type them. ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์ @@ -4574,11 +4650,19 @@ Error: %@ บันทึกข้อความต้อนรับ? No comment provided by engineer. + + Saved + No comment provided by engineer. + Saved WebRTC ICE servers will be removed เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก No comment provided by engineer. + + Saved from + No comment provided by engineer. + Saved message message info title @@ -4874,6 +4958,10 @@ Error: %@ การตั้งค่า No comment provided by engineer. + + Shape profile images + No comment provided by engineer. + Share แชร์ @@ -4989,6 +5077,14 @@ Error: %@ SimpleX links ลิงก์ SimpleX + chat feature + + + SimpleX links are prohibited in this group. + No comment provided by engineer. + + + SimpleX links not allowed No comment provided by engineer. @@ -5024,6 +5120,10 @@ Error: %@ ใครบางคน notification title + + Square, circle, or anything in between. + No comment provided by engineer. + Start chat เริ่มแชท @@ -5785,6 +5885,10 @@ To connect, please ask your contact to create another connection link and check ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. + + Voice messages not allowed + No comment provided by engineer. + Voice messages prohibited! ห้ามข้อความเสียง! @@ -5852,6 +5956,10 @@ To connect, please ask your contact to create another connection link and check เมื่อพร้อมใช้งาน No comment provided by engineer. + + When connecting audio and video calls. + No comment provided by engineer. + When people request to connect, you can accept or reject it. เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้ @@ -5862,6 +5970,18 @@ To connect, please ask your contact to create another connection link and check เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ No comment provided by engineer. + + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + No comment provided by engineer. + + + Wired ethernet + No comment provided by engineer. + With encrypted files and media. No comment provided by engineer. @@ -6323,6 +6443,10 @@ SimpleX servers cannot see your profile. ผู้ดูแลระบบ member role + + admins + feature role + agreeing encryption for %@… ยอมรับ encryption สำหรับ %@… @@ -6333,6 +6457,10 @@ SimpleX servers cannot see your profile. เห็นด้วยกับการ encryption… chat item text + + all members + feature role + always เสมอ @@ -6653,6 +6781,10 @@ SimpleX servers cannot see your profile. event happened No comment provided by engineer. + + forwarded + No comment provided by engineer. + group deleted ลบกลุ่มแล้ว @@ -6859,6 +6991,10 @@ SimpleX servers cannot see your profile. เจ้าของ member role + + owners + feature role + peer-to-peer เพื่อนต่อเพื่อน @@ -6906,6 +7042,14 @@ SimpleX servers cannot see your profile. ลบคุณออกแล้ว rcv group event chat item + + saved + No comment provided by engineer. + + + saved from %@ + No comment provided by engineer. + sec วินาที @@ -7038,6 +7182,10 @@ SimpleX servers cannot see your profile. ใช่ pref value + + you + No comment provided by engineer. + you are invited to group คุณได้รับเชิญให้เข้าร่วมกลุ่ม diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 05c68ea21e..df5076fb07 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -743,6 +743,11 @@ Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde) No comment provided by engineer. + + Allow to send SimpleX links. + SimpleX bağlantıları göndilmesine izin ver. + No comment provided by engineer. + Allow to send files and media. Dosya ve medya göndermeye izin ver. @@ -1083,6 +1088,11 @@ Dosya alınamıyor No comment provided by engineer. + + Cellular + Hücresel Veri + No comment provided by engineer. + Change Değiştir @@ -2078,6 +2088,11 @@ Bu geri alınamaz! Sürüm düşür ve sohbeti aç No comment provided by engineer. + + Download + İndir + chat item action + Download failed Yükleme başarısız oldu @@ -2188,6 +2203,11 @@ Bu geri alınamaz! Kendini imha şifresini etkinleştir set passcode view + + Enabled for + Şunlar için etkinleştirildi + No comment provided by engineer. + Encrypt Şifreleme @@ -2338,11 +2358,6 @@ Bu geri alınamaz! Üye(ler) eklenirken hata oluştu No comment provided by engineer. - - Error allowing contact PQ encryption - İletişim PQ şifrelemesine izin verirken hata oluştu - No comment provided by engineer. - Error changing address Adres değiştirilirken hata oluştu @@ -2713,6 +2728,11 @@ Bu geri alınamaz! Dosyalar ve medya bu grupta yasaklandı. No comment provided by engineer. + + Files and media not allowed + Dosyalar ve medyaya izin verilmiyor + No comment provided by engineer. + Files and media prohibited! Dosyalar ve medya yasaklandı! @@ -2778,6 +2798,26 @@ Bu geri alınamaz! Konsol için No comment provided by engineer. + + Forward + İlet + chat item action + + + Forward and save messages + Mesajları ilet ve kaydet + No comment provided by engineer. + + + Forwarded + İletildi + No comment provided by engineer. + + + Forwarded from + Şuradan iletildi + No comment provided by engineer. + Found desktop Bilgisayar bulundu @@ -2888,6 +2928,11 @@ Bu geri alınamaz! Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) No comment provided by engineer. + + Group members can send SimpleX links. + Grup üyeleri SimpleX bağlantıları gönderebilir. + No comment provided by engineer. + Group members can send direct messages. Grup üyeleri doğrudan mesajlar gönderebilir. @@ -3128,6 +3173,11 @@ Bu geri alınamaz! Cevap olarak No comment provided by engineer. + + In-call sounds + Arama içi sesler + No comment provided by engineer. + Incognito Gizli @@ -3603,6 +3653,11 @@ Bu senin grup için bağlantın %@! Mesaj tepkileri bu grupta yasaklandı. No comment provided by engineer. + + Message source remains private. + Mesaj kaynağı gizli kalır. + No comment provided by engineer. + Message text Mesaj yazısı @@ -3718,6 +3773,11 @@ Bu senin grup için bağlantın %@! Daha fazla geliştirmeler yakında geliyor! No comment provided by engineer. + + More reliable network connection. + Daha güvenilir ağ bağlantısı. + No comment provided by engineer. + Most likely this connection is deleted. Büyük ihtimalle bu bağlantı silinmiş. @@ -3753,6 +3813,16 @@ Bu senin grup için bağlantın %@! Ağ & sunucular No comment provided by engineer. + + Network connection + Ağ bağlantısı + No comment provided by engineer. + + + Network management + Ağ yönetimi + No comment provided by engineer. + Network settings Ağ ayarları @@ -3863,6 +3933,11 @@ Bu senin grup için bağlantın %@! Geçmiş yok No comment provided by engineer. + + No network connection + Ağ bağlantısı yok + No comment provided by engineer. + No permission to record voice message Sesli mesaj kaydetmek için izin yok @@ -4077,6 +4152,11 @@ Bu senin grup için bağlantın %@! Veya bu kodu göster No comment provided by engineer. + + Other + Diğer + No comment provided by engineer. + PING count PING sayısı @@ -4244,11 +4324,6 @@ Hata: %@ Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil server test error - - Post-quantum E2EE - Kuantum sonrası E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Son mesaj taslağını ekleriyle birlikte koru. @@ -4299,6 +4374,11 @@ Hata: %@ Profil fotoğrafı No comment provided by engineer. + + Profile images + Profil resimleri + No comment provided by engineer. + Profile name Profil ismi @@ -4339,6 +4419,11 @@ Hata: %@ Mesajlarda tepkileri yasakla. No comment provided by engineer. + + Prohibit sending SimpleX links. + SimpleX bağlantısı gönderimini yasakla. + No comment provided by engineer. + Prohibit sending direct messages to members. Geri dönülmez mesaj silme işlemini yasakla. @@ -4441,7 +4526,7 @@ Hata: %@ Receipts are disabled - Makbuzlar devre dışı bırakıldı + Gönderildi bilgisi devre dışı bırakıldı No comment provided by engineer. @@ -4469,6 +4554,11 @@ Hata: %@ Alıcı adresi farklı bir sunucuya değiştirilecektir. Gönderici çevrimiçi olduktan sonra adres değişikliği tamamlanacaktır. No comment provided by engineer. + + Receiving concurrency + Eşzamanlılık alınıyor + No comment provided by engineer. + Receiving file will be stopped. Dosya alımı durdurulacaktır. @@ -4484,6 +4574,11 @@ Hata: %@ Yakın geçmiş ve geliştirilmiş [dizin botu](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex. im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + Alıcı(lar) bu mesajın kimden geldiğini göremez. + No comment provided by engineer. + Recipients see updates as you type them. Alıcılar yazdığına göre güncellemeleri görecektir. @@ -4784,11 +4879,21 @@ Hata: %@ Hoşgeldin mesajı kaydedilsin mi? No comment provided by engineer. + + Saved + Kaydedildi + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Kaydedilmiş WebRTC ICE sunucuları silinecek No comment provided by engineer. + + Saved from + Tarafından kaydedildi + No comment provided by engineer. + Saved message Kaydedilmiş mesaj @@ -4961,7 +5066,7 @@ Hata: %@ Sending delivery receipts will be enabled for all contacts. - Teslimat makbuzlarının gönderilmesi tüm kişiler için etkinleştirilecektir. + Gönderildi bilgisi tüm kişiler için etkinleştirilecektir. No comment provided by engineer. @@ -5094,6 +5199,11 @@ Hata: %@ Ayarlar No comment provided by engineer. + + Shape profile images + Profil resimlerini şekillendir + No comment provided by engineer. + Share Paylaş @@ -5212,6 +5322,16 @@ Hata: %@ SimpleX links SimpleX bağlantıları + chat feature + + + SimpleX links are prohibited in this group. + SimpleX bağlantıları bu grupta yasaklandı. + No comment provided by engineer. + + + SimpleX links not allowed + SimpleX bağlantılarına izin verilmiyor No comment provided by engineer. @@ -5249,6 +5369,11 @@ Hata: %@ Biri notification title + + Square, circle, or anything in between. + Kare,daire, veya aralarında herhangi bir şey. + No comment provided by engineer. + Start chat Sohbeti başlat @@ -6050,6 +6175,11 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Bu grupta sesli mesajlar yasaktır. No comment provided by engineer. + + Voice messages not allowed + Sesli mesajlara izin verilmiyor + No comment provided by engineer. + Voice messages prohibited! Sesli mesajlar yasaktır! @@ -6120,6 +6250,11 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Mevcut olduğunda No comment provided by engineer. + + When connecting audio and video calls. + Sesli ve görüntülü aramalara bağlanırken. + No comment provided by engineer. + When people request to connect, you can accept or reject it. İnsanlar bağlantı talebinde bulunduğunda, kabul edebilir veya reddedebilirsiniz. @@ -6130,6 +6265,21 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Biriyle gizli bir profil paylaştığınızda, bu profil sizi davet ettikleri gruplar için kullanılacaktır. No comment provided by engineer. + + WiFi + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + Doğrudan sohbetlerde etkinleştirilecektir! + No comment provided by engineer. + + + Wired ethernet + Kablolu ethernet + No comment provided by engineer. + With encrypted files and media. Şifrelenmiş dosyalar ve medya ile birlikte. @@ -6613,6 +6763,11 @@ SimpleX sunucuları profilinizi göremez. yönetici member role + + admins + yöneticiler + feature role + agreeing encryption for %@… %@ için şifreleme kabul ediliyor… @@ -6623,6 +6778,11 @@ SimpleX sunucuları profilinizi göremez. şifreleme kabul ediliyor… chat item text + + all members + bütün üyeler + feature role + always her zaman @@ -6953,6 +7113,11 @@ SimpleX sunucuları profilinizi göremez. etkinlik yaşandı No comment provided by engineer. + + forwarded + iletildi + No comment provided by engineer. + group deleted grup silindi @@ -7160,6 +7325,11 @@ SimpleX sunucuları profilinizi göremez. sahip member role + + owners + sahipler + feature role + peer-to-peer eşler arası @@ -7210,6 +7380,16 @@ SimpleX sunucuları profilinizi göremez. sen kaldırıldın rcv group event chat item + + saved + kaydedildi + No comment provided by engineer. + + + saved from %@ + %@ tarafından kaydedildi + No comment provided by engineer. + sec sn @@ -7350,6 +7530,11 @@ SimpleX sunucuları profilinizi göremez. evet pref value + + you + sen + No comment provided by engineer. + you are invited to group gruba davet edildiniz diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 29edc0b9ac..1ed1f5ffd6 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -743,6 +743,10 @@ Дозволяє безповоротно видаляти надіслані повідомлення. (24 години) No comment provided by engineer. + + Allow to send SimpleX links. + No comment provided by engineer. + Allow to send files and media. Дозволяє надсилати файли та медіа. @@ -1083,6 +1087,10 @@ Не вдається отримати файл No comment provided by engineer. + + Cellular + No comment provided by engineer. + Change Зміна @@ -2078,6 +2086,10 @@ This cannot be undone! Пониження та відкритий чат No comment provided by engineer. + + Download + chat item action + Download failed Не вдалося завантажити @@ -2188,6 +2200,10 @@ This cannot be undone! Увімкнути пароль самознищення set passcode view + + Enabled for + No comment provided by engineer. + Encrypt Зашифрувати @@ -2338,11 +2354,6 @@ This cannot be undone! Помилка додавання користувача(ів) No comment provided by engineer. - - Error allowing contact PQ encryption - Помилка, що дозволяє шифрування контакту PQ - No comment provided by engineer. - Error changing address Помилка зміни адреси @@ -2713,6 +2724,10 @@ This cannot be undone! Файли та медіа в цій групі заборонені. No comment provided by engineer. + + Files and media not allowed + No comment provided by engineer. + Files and media prohibited! Файли та медіа заборонені! @@ -2778,6 +2793,22 @@ This cannot be undone! Для консолі No comment provided by engineer. + + Forward + chat item action + + + Forward and save messages + No comment provided by engineer. + + + Forwarded + No comment provided by engineer. + + + Forwarded from + No comment provided by engineer. + Found desktop Знайдено робочий стіл @@ -2888,6 +2919,10 @@ This cannot be undone! Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години) No comment provided by engineer. + + Group members can send SimpleX links. + No comment provided by engineer. + Group members can send direct messages. Учасники групи можуть надсилати прямі повідомлення. @@ -3128,6 +3163,10 @@ This cannot be undone! У відповідь на No comment provided by engineer. + + In-call sounds + No comment provided by engineer. + Incognito Інкогніто @@ -3603,6 +3642,10 @@ This is your link for group %@! Реакції на повідомлення в цій групі заборонені. No comment provided by engineer. + + Message source remains private. + No comment provided by engineer. + Message text Текст повідомлення @@ -3718,6 +3761,10 @@ This is your link for group %@! Незабаром буде ще більше покращень! No comment provided by engineer. + + More reliable network connection. + No comment provided by engineer. + Most likely this connection is deleted. Швидше за все, це з'єднання видалено. @@ -3753,6 +3800,14 @@ This is your link for group %@! Мережа та сервери No comment provided by engineer. + + Network connection + No comment provided by engineer. + + + Network management + No comment provided by engineer. + Network settings Налаштування мережі @@ -3863,6 +3918,10 @@ This is your link for group %@! Немає історії No comment provided by engineer. + + No network connection + No comment provided by engineer. + No permission to record voice message Немає дозволу на запис голосового повідомлення @@ -4077,6 +4136,10 @@ This is your link for group %@! Або покажіть цей код No comment provided by engineer. + + Other + No comment provided by engineer. + PING count Кількість PING @@ -4244,11 +4307,6 @@ Error: %@ Можливо, в адресі сервера неправильно вказано відбиток сертифіката server test error - - Post-quantum E2EE - Пост-квантовий E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. Зберегти чернетку останнього повідомлення з вкладеннями. @@ -4299,6 +4357,10 @@ Error: %@ Зображення профілю No comment provided by engineer. + + Profile images + No comment provided by engineer. + Profile name Назва профілю @@ -4339,6 +4401,10 @@ Error: %@ Заборонити реакції на повідомлення. No comment provided by engineer. + + Prohibit sending SimpleX links. + No comment provided by engineer. + Prohibit sending direct messages to members. Заборонити надсилати прямі повідомлення учасникам. @@ -4469,6 +4535,10 @@ Error: %@ Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі. No comment provided by engineer. + + Receiving concurrency + No comment provided by engineer. + Receiving file will be stopped. Отримання файлу буде зупинено. @@ -4484,6 +4554,10 @@ Error: %@ Нещодавня історія та покращення [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + No comment provided by engineer. + Recipients see updates as you type them. Одержувачі бачать оновлення, коли ви їх вводите. @@ -4784,11 +4858,19 @@ Error: %@ Зберегти вітальне повідомлення? No comment provided by engineer. + + Saved + No comment provided by engineer. + Saved WebRTC ICE servers will be removed Збережені сервери WebRTC ICE буде видалено No comment provided by engineer. + + Saved from + No comment provided by engineer. + Saved message Збережене повідомлення @@ -5094,6 +5176,10 @@ Error: %@ Налаштування No comment provided by engineer. + + Shape profile images + No comment provided by engineer. + Share Поділіться @@ -5212,6 +5298,14 @@ Error: %@ SimpleX links Посилання SimpleX + chat feature + + + SimpleX links are prohibited in this group. + No comment provided by engineer. + + + SimpleX links not allowed No comment provided by engineer. @@ -5249,6 +5343,10 @@ Error: %@ Хтось notification title + + Square, circle, or anything in between. + No comment provided by engineer. + Start chat Почати чат @@ -6050,6 +6148,10 @@ To connect, please ask your contact to create another connection link and check Голосові повідомлення в цій групі заборонені. No comment provided by engineer. + + Voice messages not allowed + No comment provided by engineer. + Voice messages prohibited! Голосові повідомлення заборонені! @@ -6120,6 +6222,10 @@ To connect, please ask your contact to create another connection link and check За наявності No comment provided by engineer. + + When connecting audio and video calls. + No comment provided by engineer. + When people request to connect, you can accept or reject it. Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його. @@ -6130,6 +6236,18 @@ To connect, please ask your contact to create another connection link and check Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують. No comment provided by engineer. + + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + No comment provided by engineer. + + + Wired ethernet + No comment provided by engineer. + With encrypted files and media. З зашифрованими файлами та медіа. @@ -6613,6 +6731,10 @@ SimpleX servers cannot see your profile. адмін member role + + admins + feature role + agreeing encryption for %@… узгодження шифрування для %@… @@ -6623,6 +6745,10 @@ SimpleX servers cannot see your profile. узгодження шифрування… chat item text + + all members + feature role + always завжди @@ -6953,6 +7079,10 @@ SimpleX servers cannot see your profile. відбулася подія No comment provided by engineer. + + forwarded + No comment provided by engineer. + group deleted групу видалено @@ -7160,6 +7290,10 @@ SimpleX servers cannot see your profile. власник member role + + owners + feature role + peer-to-peer одноранговий @@ -7210,6 +7344,14 @@ SimpleX servers cannot see your profile. прибрали вас rcv group event chat item + + saved + No comment provided by engineer. + + + saved from %@ + No comment provided by engineer. + sec сек @@ -7350,6 +7492,10 @@ SimpleX servers cannot see your profile. так pref value + + you + No comment provided by engineer. + you are invited to group вас запрошують до групи diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 3bf27bff9c..4bf9e05665 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -728,6 +728,10 @@ 允许不可撤回地删除已发送消息。 No comment provided by engineer. + + Allow to send SimpleX links. + No comment provided by engineer. + Allow to send files and media. 允许发送文件和媒体。 @@ -1063,6 +1067,10 @@ 无法接收文件 No comment provided by engineer. + + Cellular + No comment provided by engineer. + Change 更改 @@ -2039,6 +2047,10 @@ This cannot be undone! 降级并打开聊天 No comment provided by engineer. + + Download + chat item action + Download failed No comment provided by engineer. @@ -2145,6 +2157,10 @@ This cannot be undone! 启用自毁密码 set passcode view + + Enabled for + No comment provided by engineer. + Encrypt 加密 @@ -2291,10 +2307,6 @@ This cannot be undone! 添加成员错误 No comment provided by engineer. - - Error allowing contact PQ encryption - No comment provided by engineer. - Error changing address 更改地址错误 @@ -2658,6 +2670,10 @@ This cannot be undone! 此群组中禁止文件和媒体。 No comment provided by engineer. + + Files and media not allowed + No comment provided by engineer. + Files and media prohibited! 禁止文件和媒体! @@ -2721,6 +2737,22 @@ This cannot be undone! 用于控制台 No comment provided by engineer. + + Forward + chat item action + + + Forward and save messages + No comment provided by engineer. + + + Forwarded + No comment provided by engineer. + + + Forwarded from + No comment provided by engineer. + Found desktop 找到了桌面 @@ -2830,6 +2862,10 @@ This cannot be undone! 群组成员可以不可撤回地删除已发送的消息。 No comment provided by engineer. + + Group members can send SimpleX links. + No comment provided by engineer. + Group members can send direct messages. 群组成员可以私信。 @@ -3066,6 +3102,10 @@ This cannot be undone! 答复 No comment provided by engineer. + + In-call sounds + No comment provided by engineer. + Incognito 隐身聊天 @@ -3534,6 +3574,10 @@ This is your link for group %@! 该群组禁用了消息回应。 No comment provided by engineer. + + Message source remains private. + No comment provided by engineer. + Message text 消息正文 @@ -3638,6 +3682,10 @@ This is your link for group %@! 更多改进即将推出! No comment provided by engineer. + + More reliable network connection. + No comment provided by engineer. + Most likely this connection is deleted. 此连接很可能已被删除。 @@ -3673,6 +3721,14 @@ This is your link for group %@! 网络和服务器 No comment provided by engineer. + + Network connection + No comment provided by engineer. + + + Network management + No comment provided by engineer. + Network settings 网络设置 @@ -3783,6 +3839,10 @@ This is your link for group %@! 无历史记录 No comment provided by engineer. + + No network connection + No comment provided by engineer. + No permission to record voice message 没有录制语音消息的权限 @@ -3993,6 +4053,10 @@ This is your link for group %@! 或者显示此码 No comment provided by engineer. + + Other + No comment provided by engineer. + PING count PING 次数 @@ -4155,10 +4219,6 @@ Error: %@ 服务器地址中的证书指纹可能不正确 server test error - - Post-quantum E2EE - No comment provided by engineer. - Preserve the last message draft, with attachments. 保留最后的消息草稿及其附件。 @@ -4209,6 +4269,10 @@ Error: %@ 资料图片 No comment provided by engineer. + + Profile images + No comment provided by engineer. + Profile name No comment provided by engineer. @@ -4248,6 +4312,10 @@ Error: %@ 禁止消息回应。 No comment provided by engineer. + + Prohibit sending SimpleX links. + No comment provided by engineer. + Prohibit sending direct messages to members. 禁止向成员发送私信。 @@ -4376,6 +4444,10 @@ Error: %@ 接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。 No comment provided by engineer. + + Receiving concurrency + No comment provided by engineer. + Receiving file will be stopped. 即将停止接收文件。 @@ -4390,6 +4462,10 @@ Error: %@ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). No comment provided by engineer. + + Recipient(s) can't see who this message is from. + No comment provided by engineer. + Recipients see updates as you type them. 对方会在您键入时看到更新。 @@ -4686,11 +4762,19 @@ Error: %@ 保存欢迎信息? No comment provided by engineer. + + Saved + No comment provided by engineer. + Saved WebRTC ICE servers will be removed 已保存的WebRTC ICE服务器将被删除 No comment provided by engineer. + + Saved from + No comment provided by engineer. + Saved message 已保存的消息 @@ -4995,6 +5079,10 @@ Error: %@ 设置 No comment provided by engineer. + + Shape profile images + No comment provided by engineer. + Share 分享 @@ -5112,6 +5200,14 @@ Error: %@ SimpleX links SimpleX 链接 + chat feature + + + SimpleX links are prohibited in this group. + No comment provided by engineer. + + + SimpleX links not allowed No comment provided by engineer. @@ -5149,6 +5245,10 @@ Error: %@ 某人 notification title + + Square, circle, or anything in between. + No comment provided by engineer. + Start chat 开始聊天 @@ -5939,6 +6039,10 @@ To connect, please ask your contact to create another connection link and check 语音信息在该群组中被禁用。 No comment provided by engineer. + + Voice messages not allowed + No comment provided by engineer. + Voice messages prohibited! 语音消息禁止发送! @@ -6006,6 +6110,10 @@ To connect, please ask your contact to create another connection link and check 当可用时 No comment provided by engineer. + + When connecting audio and video calls. + No comment provided by engineer. + When people request to connect, you can accept or reject it. 当人们请求连接时,您可以接受或拒绝它。 @@ -6016,6 +6124,18 @@ To connect, please ask your contact to create another connection link and check 当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。 No comment provided by engineer. + + WiFi + No comment provided by engineer. + + + Will be enabled in direct chats! + No comment provided by engineer. + + + Wired ethernet + No comment provided by engineer. + With encrypted files and media. 加密的文件和媒体。 @@ -6487,6 +6607,10 @@ SimpleX 服务器无法看到您的资料。 管理员 member role + + admins + feature role + agreeing encryption for %@… 正在协商将加密应用于 %@… @@ -6497,6 +6621,10 @@ SimpleX 服务器无法看到您的资料。 同意加密… chat item text + + all members + feature role + always 始终 @@ -6825,6 +6953,10 @@ SimpleX 服务器无法看到您的资料。 发生的事 No comment provided by engineer. + + forwarded + No comment provided by engineer. + group deleted 群组已删除 @@ -7031,6 +7163,10 @@ SimpleX 服务器无法看到您的资料。 群主 member role + + owners + feature role + peer-to-peer 点对点 @@ -7080,6 +7216,14 @@ SimpleX 服务器无法看到您的资料。 已将您移除 rcv group event chat item + + saved + No comment provided by engineer. + + + saved from %@ + No comment provided by engineer. + sec 秒 @@ -7217,6 +7361,10 @@ SimpleX 服务器无法看到您的资料。 是 pref value + + you + No comment provided by engineer. + you are invited to group 您被邀请加入群组 diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index efd8f33dd8..faa7f4f44c 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -586,9 +586,6 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { cleanupDirectFile(aChatItem) } return nil - case let .sndFileCompleteXFTP(_, aChatItem, _): - cleanupFile(aChatItem) - return nil case let .callInvitation(invitation): // Do not post it without CallKit support, iOS will stop launching the app without showing CallKit return ( diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 6443c21b9e..30345d6895 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -111,15 +111,15 @@ 5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; }; 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 */; }; - 5CC932F52BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */; }; - 5CC932F62BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */; }; - 5CC932F72BBDD9FA008A1EB6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */; }; - 5CC932F82BBDD9FA008A1EB6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F32BBDD9F9008A1EB6 /* libgmp.a */; }; - 5CC932F92BBDD9FA008A1EB6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F42BBDD9F9008A1EB6 /* libffi.a */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; + 5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */; }; + 5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */; }; + 5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */; }; + 5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */; }; + 5CE0E8AF2BF0C1B5008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; }; @@ -172,6 +172,7 @@ 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; }; 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; }; 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; + 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; @@ -187,6 +188,9 @@ 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; }; 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; }; 8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; }; + 8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; }; + 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; }; + 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; }; D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; }; D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; }; @@ -402,11 +406,6 @@ 5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 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 = ""; }; - 5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a"; sourceTree = ""; }; - 5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a"; sourceTree = ""; }; - 5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CC932F32BBDD9F9008A1EB6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CC932F42BBDD9F9008A1EB6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = ""; }; @@ -421,6 +420,11 @@ 5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = ""; }; 5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; 5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; + 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a"; sourceTree = ""; }; + 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a"; sourceTree = ""; }; + 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -463,6 +467,7 @@ 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = ""; }; 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = ""; }; 648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = ""; }; + 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; @@ -480,6 +485,9 @@ 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = ""; }; 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = ""; }; + 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDevicePicker.swift; sourceTree = ""; }; + 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = ""; }; + 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = ""; }; D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -522,12 +530,12 @@ buildActionMask = 2147483647; files = ( 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CC932F72BBDD9FA008A1EB6 /* libgmpxx.a in Frameworks */, - 5CC932F62BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a in Frameworks */, - 5CC932F52BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a in Frameworks */, - 5CC932F92BBDD9FA008A1EB6 /* libffi.a in Frameworks */, - 5CC932F82BBDD9FA008A1EB6 /* libgmp.a in Frameworks */, + 5CE0E8AE2BF0C1B5008D6E06 /* libffi.a in Frameworks */, + 5CE0E8AD2BF0C1B5008D6E06 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + 5CE0E8AC2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a in Frameworks */, + 5CE0E8AB2BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a in Frameworks */, + 5CE0E8AF2BF0C1B5008D6E06 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -545,6 +553,8 @@ 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */, 18415323A4082FC92887F906 /* WebRTCClient.swift */, 18415B08031E8FB0F7FC27F9 /* CallViewRenderers.swift */, + 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */, + 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */, ); path = Call; sourceTree = ""; @@ -583,6 +593,7 @@ 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */, 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, + 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */, ); path = Chat; sourceTree = ""; @@ -590,11 +601,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CC932F42BBDD9F9008A1EB6 /* libffi.a */, - 5CC932F32BBDD9F9008A1EB6 /* libgmp.a */, - 5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */, - 5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */, - 5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */, + 5CE0E8A92BF0C1B5008D6E06 /* libffi.a */, + 5CE0E8A82BF0C1B5008D6E06 /* libgmp.a */, + 5CE0E8AA2BF0C1B5008D6E06 /* libgmpxx.a */, + 5CE0E8A62BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg-ghc9.6.3.a */, + 5CE0E8A72BF0C1B5008D6E06 /* libHSsimplex-chat-5.7.3.0-BQ0iWJUV1AuAQdR2Nu1hMg.a */, ); path = Libraries; sourceTree = ""; @@ -623,6 +634,7 @@ 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */, 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */, 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, + 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */, ); path = Model; sourceTree = ""; @@ -1158,6 +1170,7 @@ 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */, 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, + 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */, 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, @@ -1176,6 +1189,7 @@ 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */, 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */, + 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, 5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */, @@ -1229,6 +1243,7 @@ 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */, 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */, 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, + 8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */, 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, @@ -1258,6 +1273,7 @@ 18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */, 1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */, 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */, + 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */, 18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */, 184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */, 5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */, @@ -1536,7 +1552,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 215; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1561,7 +1577,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 5.6.1; + MARKETING_VERSION = 5.7.3; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1585,7 +1601,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 215; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1610,7 +1626,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6.1; + MARKETING_VERSION = 5.7.3; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1671,7 +1687,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 215; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1686,7 +1702,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6.1; + MARKETING_VERSION = 5.7.3; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1708,7 +1724,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 215; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1723,7 +1739,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6.1; + MARKETING_VERSION = 5.7.3; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1745,7 +1761,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 215; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1771,7 +1787,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6.1; + MARKETING_VERSION = 5.7.3; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1796,7 +1812,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 206; + CURRENT_PROJECT_VERSION = 215; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1822,7 +1838,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6.1; + MARKETING_VERSION = 5.7.3; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index cdd1008c13..e2f4adc60f 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -87,6 +87,11 @@ public func chatInitControllerRemovingDatabases() { public func chatCloseStore() { + // Prevent crash when exiting the app with already closed store (for example, after changing a database passpharase) + guard hasChatCtrl() else { + logger.error("chatCloseStore: already closed, chatCtrl is nil") + return + } let err = fromCString(chat_close_store(getChatCtrl())) if err != "" { logger.error("chatCloseStore error: \(err)") diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index b18022cfec..3aa610e4af 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -32,8 +32,6 @@ public enum ChatCommand { case setTempFolder(tempFolder: String) case setFilesFolder(filesFolder: String) case apiSetEncryptLocalFiles(enable: Bool) - case apiSetPQEncryption(enable: Bool) - case apiSetContactPQ(contactId: Int64, enable: Bool) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage @@ -50,6 +48,7 @@ public enum ChatCommand { case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) + case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -77,6 +76,7 @@ public enum ChatCommand { case apiGetChatItemTTL(userId: Int64) case apiSetNetworkConfig(networkConfig: NetCfg) case apiGetNetworkConfig + case apiSetNetworkInfo(networkInfo: UserNetworkInfo) case reconnectAllServers case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) @@ -170,8 +170,6 @@ public enum ChatCommand { case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" - case let .apiSetPQEncryption(enable): return "/pq \(onOff(enable))" - case let .apiSetContactPQ(contactId, enable): return "/_pq @\(contactId) \(onOff(enable))" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" @@ -194,6 +192,7 @@ public enum ChatCommand { case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)" case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" + case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId): return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId)" case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" @@ -221,6 +220,7 @@ public enum ChatCommand { case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" case .apiGetNetworkConfig: return "/network" + case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))" case .reconnectAllServers: return "/reconnect" case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" @@ -322,8 +322,6 @@ public enum ChatCommand { case .setTempFolder: return "setTempFolder" case .setFilesFolder: return "setFilesFolder" case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles" - case .apiSetPQEncryption: return "apiSetPQEncryption" - case .apiSetContactPQ: return "apiSetContactPQ" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" @@ -341,6 +339,7 @@ public enum ChatCommand { case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" case .apiChatItemReaction: return "apiChatItemReaction" + case .apiForwardChatItem: return "apiForwardChatItem" case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" @@ -368,6 +367,7 @@ public enum ChatCommand { case .apiGetChatItemTTL: return "apiGetChatItemTTL" case .apiSetNetworkConfig: return "apiSetNetworkConfig" case .apiGetNetworkConfig: return "apiGetNetworkConfig" + case .apiSetNetworkInfo: return "apiSetNetworkInfo" case .reconnectAllServers: return "reconnectAllServers" case .apiSetChatSettings: return "apiSetChatSettings" case .apiSetMemberSettings: return "apiSetMemberSettings" @@ -647,7 +647,6 @@ public enum ChatResponse: Decodable, Error { case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo) case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason) // pq - case contactPQAllowed(user: UserRef, contact: Contact, pqEncryption: Bool) case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool) // misc case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) @@ -803,8 +802,7 @@ public enum ChatResponse: Decodable, Error { case .remoteCtrlSessionCode: return "remoteCtrlSessionCode" case .remoteCtrlConnected: return "remoteCtrlConnected" case .remoteCtrlStopped: return "remoteCtrlStopped" - case .contactPQAllowed: return "contactPQAllowed" - case .contactPQEnabled: return "contactPQAllowed" + case .contactPQEnabled: return "contactPQEnabled" case .versionInfo: return "versionInfo" case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" @@ -961,7 +959,6 @@ public enum ChatResponse: Decodable, Error { case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)" case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) case .remoteCtrlStopped: return noDetails - case let .contactPQAllowed(u, contact, pqEncryption): return withUser(u, "contact: \(String(describing: contact))\npqEncryption: \(pqEncryption)") case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" case .cmdOk: return noDetails @@ -1251,6 +1248,7 @@ public struct NetCfg: Codable, Equatable { public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds + public var rcvConcurrency: Int // pool size public var tcpKeepAlive: KeepAliveOpts? public var smpPingInterval: Int // microseconds public var smpPingCount: Int // times @@ -1259,9 +1257,10 @@ public struct NetCfg: Codable, Equatable { public static let defaults: NetCfg = NetCfg( socksProxy: nil, sessionMode: TransportSessionMode.user, - tcpConnectTimeout: 20_000_000, + tcpConnectTimeout: 25_000_000, tcpTimeout: 15_000_000, - tcpTimeoutPerKb: 45_000, + tcpTimeoutPerKb: 10_000, + rcvConcurrency: 12, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, @@ -1271,9 +1270,10 @@ public struct NetCfg: Codable, Equatable { public static let proxyDefaults: NetCfg = NetCfg( socksProxy: nil, sessionMode: TransportSessionMode.user, - tcpConnectTimeout: 30_000_000, + tcpConnectTimeout: 35_000_000, tcpTimeout: 20_000_000, - tcpTimeoutPerKb: 60_000, + tcpTimeoutPerKb: 15_000, + rcvConcurrency: 8, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, @@ -1714,6 +1714,8 @@ public enum ChatErrorType: Decodable { case fallbackToSMPProhibited(fileId: Int64) case inlineFileProhibited(fileId: Int64) case invalidQuote + case invalidForward + case forwardNoFile case invalidChatItemUpdate case invalidChatItemDelete case hasCurrentCall @@ -2086,3 +2088,31 @@ public enum AppSettingsLockScreenCalls: String, Codable { case show case accept } + +public struct UserNetworkInfo: Codable, Equatable { + public let networkType: UserNetworkType + public let online: Bool + + public init(networkType: UserNetworkType, online: Bool) { + self.networkType = networkType + self.online = online + } +} + +public enum UserNetworkType: String, Codable { + case none + case cellular + case wifi + case ethernet + case other + + public var text: LocalizedStringKey { + switch self { + case .none: "No network connection" + case .cellular: "Cellular" + case .wifi: "WiFi" + case .ethernet: "Wired ethernet" + case .other: "Other" + } + } +} diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index df4de134c2..0511a8486c 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -29,6 +29,7 @@ let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" +let GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval" let GROUP_DEFAULT_NETWORK_SMP_PING_COUNT = "networkSMPPingCount" let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive" @@ -40,7 +41,7 @@ let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" -public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" +public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used public let APP_GROUP_NAME = "group.chat.simplex.app" @@ -55,6 +56,7 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, + GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY: NetCfg.defaults.rcvConcurrency, GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval, GROUP_DEFAULT_NETWORK_SMP_PING_COUNT: NetCfg.defaults.smpPingCount, GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive, @@ -197,8 +199,6 @@ public let confirmDBUpgradesGroupDefault = BoolDefault(defaults: groupDefaults, public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED) -public let pqExperimentalEnabledDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED) - public class DateDefault { var defaults: UserDefaults var key: String @@ -278,6 +278,7 @@ public func getNetCfg() -> NetCfg { let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) + let rcvConcurrency = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) let smpPingCount = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT) let enableKeepAlive = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE) @@ -297,6 +298,7 @@ public func getNetCfg() -> NetCfg { tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpTimeoutPerKb: tcpTimeoutPerKb, + rcvConcurrency: rcvConcurrency, tcpKeepAlive: tcpKeepAlive, smpPingInterval: smpPingInterval, smpPingCount: smpPingCount, @@ -310,6 +312,7 @@ public func setNetCfg(_ cfg: NetCfg) { groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) + groupDefaults.set(cfg.rcvConcurrency, forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) groupDefaults.set(cfg.smpPingCount, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT) if let tcpKeepAlive = cfg.tcpKeepAlive { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index ed62b5c9ac..24aca0dd18 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -543,6 +543,7 @@ public protocol Feature { var iconFilled: String { get } var iconScale: CGFloat { get } var hasParam: Bool { get } + var hasRole: Bool { get } var text: String { get } } @@ -569,6 +570,8 @@ public enum ChatFeature: String, Decodable, Feature { } } + public var hasRole: Bool { false } + public var text: String { switch self { case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature") @@ -694,6 +697,7 @@ public enum GroupFeature: String, Decodable, Feature { case reactions case voice case files + case simplexLinks case history public var id: Self { self } @@ -705,6 +709,19 @@ public enum GroupFeature: String, Decodable, Feature { } } + public var hasRole: Bool { + switch self { + case .timedMessages: false + case .directMessages: true + case .fullDelete: false + case .reactions: false + case .voice: true + case .files: true + case .simplexLinks: true + case .history: false + } + } + public var text: String { switch self { case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature") @@ -713,6 +730,7 @@ public enum GroupFeature: String, Decodable, Feature { case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature") case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") case .files: return NSLocalizedString("Files and media", comment: "chat feature") + case .simplexLinks: return NSLocalizedString("SimpleX links", comment: "chat feature") case .history: return NSLocalizedString("Visible history", comment: "chat feature") } } @@ -725,6 +743,7 @@ public enum GroupFeature: String, Decodable, Feature { case .reactions: return "face.smiling" case .voice: return "mic" case .files: return "doc" + case .simplexLinks: return "link.circle" case .history: return "clock" } } @@ -737,6 +756,7 @@ public enum GroupFeature: String, Decodable, Feature { case .reactions: return "face.smiling.fill" case .voice: return "mic.fill" case .files: return "doc.fill" + case .simplexLinks: return "link.circle.fill" case .history: return "clock.fill" } } @@ -781,6 +801,11 @@ public enum GroupFeature: String, Decodable, Feature { case .on: return "Allow to send files and media." case .off: return "Prohibit sending files and media." } + case .simplexLinks: + switch enabled { + case .on: return "Allow to send SimpleX links." + case .off: return "Prohibit sending SimpleX links." + } case .history: switch enabled { case .on: return "Send up to 100 last messages to new members." @@ -819,6 +844,11 @@ public enum GroupFeature: String, Decodable, Feature { case .on: return "Group members can send files and media." case .off: return "Files and media are prohibited in this group." } + case .simplexLinks: + switch enabled { + case .on: return "Group members can send SimpleX links." + case .off: return "SimpleX links are prohibited in this group." + } case .history: switch enabled { case .on: return "Up to 100 last messages are sent to new members." @@ -958,20 +988,22 @@ public enum FeatureAllowed: String, Codable, Identifiable { public struct FullGroupPreferences: Decodable, Equatable { public var timedMessages: TimedMessagesGroupPreference - public var directMessages: GroupPreference + public var directMessages: RoleGroupPreference public var fullDelete: GroupPreference public var reactions: GroupPreference - public var voice: GroupPreference - public var files: GroupPreference + public var voice: RoleGroupPreference + public var files: RoleGroupPreference + public var simplexLinks: RoleGroupPreference public var history: GroupPreference public init( timedMessages: TimedMessagesGroupPreference, - directMessages: GroupPreference, + directMessages: RoleGroupPreference, fullDelete: GroupPreference, reactions: GroupPreference, - voice: GroupPreference, - files: GroupPreference, + voice: RoleGroupPreference, + files: RoleGroupPreference, + simplexLinks: RoleGroupPreference, history: GroupPreference ) { self.timedMessages = timedMessages @@ -980,36 +1012,40 @@ public struct FullGroupPreferences: Decodable, Equatable { self.reactions = reactions self.voice = voice self.files = files + self.simplexLinks = simplexLinks self.history = history } public static let sampleData = FullGroupPreferences( timedMessages: TimedMessagesGroupPreference(enable: .off), - directMessages: GroupPreference(enable: .off), + directMessages: RoleGroupPreference(enable: .off, role: nil), fullDelete: GroupPreference(enable: .off), reactions: GroupPreference(enable: .on), - voice: GroupPreference(enable: .on), - files: GroupPreference(enable: .on), + voice: RoleGroupPreference(enable: .on, role: nil), + files: RoleGroupPreference(enable: .on, role: nil), + simplexLinks: RoleGroupPreference(enable: .on, role: nil), history: GroupPreference(enable: .on) ) } public struct GroupPreferences: Codable { public var timedMessages: TimedMessagesGroupPreference? - public var directMessages: GroupPreference? + public var directMessages: RoleGroupPreference? public var fullDelete: GroupPreference? public var reactions: GroupPreference? - public var voice: GroupPreference? - public var files: GroupPreference? + public var voice: RoleGroupPreference? + public var files: RoleGroupPreference? + public var simplexLinks: RoleGroupPreference? public var history: GroupPreference? public init( timedMessages: TimedMessagesGroupPreference? = nil, - directMessages: GroupPreference? = nil, + directMessages: RoleGroupPreference? = nil, fullDelete: GroupPreference? = nil, reactions: GroupPreference? = nil, - voice: GroupPreference? = nil, - files: GroupPreference? = nil, + voice: RoleGroupPreference? = nil, + files: RoleGroupPreference? = nil, + simplexLinks: RoleGroupPreference? = nil, history: GroupPreference? = nil ) { self.timedMessages = timedMessages @@ -1018,16 +1054,18 @@ public struct GroupPreferences: Codable { self.reactions = reactions self.voice = voice self.files = files + self.simplexLinks = simplexLinks self.history = history } public static let sampleData = GroupPreferences( timedMessages: TimedMessagesGroupPreference(enable: .off), - directMessages: GroupPreference(enable: .off), + directMessages: RoleGroupPreference(enable: .off, role: nil), fullDelete: GroupPreference(enable: .off), reactions: GroupPreference(enable: .on), - voice: GroupPreference(enable: .on), - files: GroupPreference(enable: .on), + voice: RoleGroupPreference(enable: .on, role: nil), + files: RoleGroupPreference(enable: .on, role: nil), + simplexLinks: RoleGroupPreference(enable: .on, role: nil), history: GroupPreference(enable: .on) ) } @@ -1040,6 +1078,7 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group reactions: fullPreferences.reactions, voice: fullPreferences.voice, files: fullPreferences.files, + simplexLinks: fullPreferences.simplexLinks, history: fullPreferences.history ) } @@ -1051,11 +1090,37 @@ public struct GroupPreference: Codable, Equatable { enable == .on } + public func enabled(_ role: GroupMemberRole?, for m: GroupMember?) -> GroupFeatureEnabled { + switch enable { + case .off: .off + case .on: + if let role, let m { + m.memberRole >= role ? .on : .off + } else { + .on + } + } + } + public init(enable: GroupFeatureEnabled) { self.enable = enable } } +public struct RoleGroupPreference: Codable, Equatable { + public var enable: GroupFeatureEnabled + public var role: GroupMemberRole? + + public func on(for m: GroupMember) -> Bool { + enable == .on && m.memberRole >= (role ?? .observer) + } + + public init(enable: GroupFeatureEnabled, role: GroupMemberRole?) { + self.enable = enable + self.role = role + } +} + public struct TimedMessagesGroupPreference: Codable, Equatable { public var enable: GroupFeatureEnabled public var ttl: Int? @@ -1280,7 +1345,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case .timedMessages: return prefs.timedMessages.on case .fullDelete: return prefs.fullDelete.on case .reactions: return prefs.reactions.on - case .voice: return prefs.voice.on + case .voice: return prefs.voice.on(for: groupInfo.membership) case .calls: return false } case .local: @@ -1323,7 +1388,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { return .other } case let .group(groupInfo): - if !groupInfo.fullGroupPreferences.voice.on { + if !groupInfo.fullGroupPreferences.voice.on(for: groupInfo.membership) { return .groupOwnerCan } else { return .other @@ -1975,7 +2040,7 @@ public struct GroupMemberIds: Decodable { var groupId: Int64 } -public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable { +public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable { case observer = "observer" case author = "author" case member = "member" @@ -2372,10 +2437,20 @@ public struct ChatItem: Identifiable, Decodable { } } - public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> ChatItem { + public var showLocalDelete: Bool { + switch content { + case .sndDirectE2EEInfo: return false + case .rcvDirectE2EEInfo: return false + case .sndGroupE2EEInfo: return false + case .rcvGroupE2EEInfo: return false + default: return true + } + } + + public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> ChatItem { ChatItem( chatDir: dir, - meta: CIMeta.getSample(id, ts, text, status, itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, editable: editable), + meta: CIMeta.getSample(id, ts, text, status, itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, deletable: deletable, editable: editable), content: .sndMsgContent(msgContent: .text(text)), quotedItem: quotedItem, file: file @@ -2466,6 +2541,7 @@ public struct ChatItem: Identifiable, Decodable { itemDeleted: nil, itemEdited: false, itemLive: false, + deletable: false, editable: false ), content: .rcvDeleted(deleteMode: .cidmBroadcast), @@ -2487,6 +2563,7 @@ public struct ChatItem: Identifiable, Decodable { itemDeleted: nil, itemEdited: false, itemLive: true, + deletable: false, editable: false ), content: .sndMsgContent(msgContent: .text("")), @@ -2546,10 +2623,12 @@ public struct CIMeta: Decodable { public var itemStatus: CIStatus public var createdAt: Date public var updatedAt: Date + public var itemForwarded: CIForwardedFrom? public var itemDeleted: CIDeleted? public var itemEdited: Bool public var itemTimed: CITimed? public var itemLive: Bool? + public var deletable: Bool public var editable: Bool public var timestampText: Text { get { formatTimestampText(itemTs) } } @@ -2566,7 +2645,7 @@ public struct CIMeta: Decodable { itemStatus.statusIcon(metaColor) } - public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> CIMeta { + public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> CIMeta { CIMeta( itemId: id, itemTs: ts, @@ -2577,6 +2656,7 @@ public struct CIMeta: Decodable { itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, + deletable: deletable, editable: editable ) } @@ -2592,6 +2672,7 @@ public struct CIMeta: Decodable { itemDeleted: nil, itemEdited: false, itemLive: false, + deletable: false, editable: false ) } @@ -2713,6 +2794,31 @@ public enum CIDeleted: Decodable { } } +public enum MsgDirection: String, Decodable { + case rcv = "rcv" + case snd = "snd" +} + +public enum CIForwardedFrom: Decodable { + case unknown + case contact(chatName: String, msgDir: MsgDirection, contactId: Int64?, chatItemId: Int64?) + case group(chatName: String, msgDir: MsgDirection, groupId: Int64?, chatItemId: Int64?) + + var chatName: String { + switch self { + case .unknown: "" + case let .contact(chatName, _, _, _): chatName + case let .group(chatName, _, _, _): chatName + } + } + + public func text(_ chatType: ChatType) -> LocalizedStringKey { + chatType == .local + ? (chatName == "" ? "saved" : "saved from \(chatName)") + : "forwarded" + } +} + public enum CIDeleteMode: String, Decodable { case cidmBroadcast = "broadcast" case cidmInternal = "internal" @@ -2742,8 +2848,8 @@ public enum CIContent: Decodable, ItemContent { case sndChatFeature(feature: ChatFeature, enabled: FeatureEnabled, param: Int?) case rcvChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?) case sndChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?) - case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?) - case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?) + case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?) + case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?) case rcvChatFeatureRejected(feature: ChatFeature) case rcvGroupFeatureRejected(groupFeature: GroupFeature) case sndModerated @@ -2777,8 +2883,8 @@ public enum CIContent: Decodable, ItemContent { case let .sndChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param) case let .rcvChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param) case let .sndChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param) - case let .rcvGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param) - case let .sndGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param) + case let .rcvGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role) + case let .sndGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role) case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text) case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text) case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item") @@ -2803,10 +2909,25 @@ public enum CIContent: Decodable, ItemContent { NSLocalizedString("This chat is protected by end-to-end encryption.", comment: "E2EE info chat item") } - static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String { - feature.hasParam - ? "\(feature.text): \(timeText(param))" - : "\(feature.text): \(enabled)" + static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?, _ role: GroupMemberRole? = nil) -> String { + ( + feature.hasParam + ? "\(feature.text): \(timeText(param))" + : "\(feature.text): \(enabled)" + ) + + ( + feature.hasRole && role != nil + ? " (\(roleText(role)))" + : "" + ) + } + + private static func roleText(_ role: GroupMemberRole?) -> String { + switch role { + case .owner: NSLocalizedString("owners", comment: "feature role") + case .admin: NSLocalizedString("admins", comment: "feature role") + default: NSLocalizedString("all members", comment: "feature role") + } } public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String { @@ -3751,6 +3872,7 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable { public struct ChatItemInfo: Decodable { public var itemVersions: [ChatItemVersion] public var memberDeliveryStatuses: [MemberDeliveryStatus]? + public var forwardedFromChatItem: AChatItem? } public struct ChatItemVersion: Decodable { diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 44b4c6de9f..06d9f1f43e 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -85,6 +85,9 @@ /* No comment provided by engineer. */ "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението)."; +/* No comment provided by engineer. */ +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Моля, обърнете внимание**: използването на една и съща база данни на две устройства ще наруши декриптирането на съобщенията от вашите връзки като защита на сигурността."; + /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите."; @@ -94,6 +97,9 @@ /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: Незабавните push известия изискват парола, запазена в Keychain."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Внимание**: архивът ще бъде изтрит."; + /* No comment provided by engineer. */ "*bold*" = "\\*удебелен*"; @@ -136,6 +142,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ свързан"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ изтеглено"; + /* notification title */ "%@ is connected!" = "%@ е свързан!"; @@ -148,6 +157,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ сървъри"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ качено"; + /* notification title */ "%@ wants to connect!" = "%@ иска да се свърже!"; @@ -377,6 +389,9 @@ /* member role */ "admin" = "админ"; +/* feature role */ +"admins" = "администратори"; + /* No comment provided by engineer. */ "Admins can block a member for all." = "Администраторите могат да блокират член за всички."; @@ -404,6 +419,9 @@ /* No comment provided by engineer. */ "All group members will remain connected." = "Всички членове на групата ще останат свързани."; +/* feature role */ +"all members" = "всички членове"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Всички съобщения ще бъдат изтрити - това не може да бъде отменено!"; @@ -452,6 +470,9 @@ /* No comment provided by engineer. */ "Allow to send files and media." = "Позволи изпращане на файлове и медия."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Разрешаване на изпращане на SimpleX линкове."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Позволи изпращане на гласови съобщения."; @@ -695,6 +716,9 @@ /* No comment provided by engineer. */ "Cannot receive file" = "Файлът не може да бъде получен"; +/* No comment provided by engineer. */ +"Cellular" = "Мобилна мрежа"; + /* No comment provided by engineer. */ "Change" = "Промени"; @@ -780,6 +804,9 @@ /* No comment provided by engineer. */ "Chinese and Spanish interface" = "Китайски и Испански интерфейс"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "Изберете _Мигриране от друго устройство_ на новото устройство и сканирайте QR кода."; + /* No comment provided by engineer. */ "Choose file" = "Избери файл"; @@ -1386,6 +1413,9 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Понижи версията и отвори чата"; +/* chat item action */ +"Download" = "Изтегли"; + /* No comment provided by engineer. */ "Download failed" = "Неуспешно изтегляне"; @@ -1461,6 +1491,9 @@ /* enabled status */ "enabled" = "активирано"; +/* No comment provided by engineer. */ +"Enabled for" = "Активирано за"; + /* enabled status */ "enabled for contact" = "активирано за контакт"; @@ -1590,9 +1623,6 @@ /* No comment provided by engineer. */ "Error adding member(s)" = "Грешка при добавяне на член(ове)"; -/* No comment provided by engineer. */ -"Error allowing contact PQ encryption" = "Грешка при разрешаване на PQ криптиране за контакт"; - /* No comment provided by engineer. */ "Error changing address" = "Грешка при промяна на адреса"; @@ -1818,6 +1848,9 @@ /* No comment provided by engineer. */ "Files and media are prohibited in this group." = "Файловете и медията са забранени в тази група."; +/* No comment provided by engineer. */ +"Files and media not allowed" = "Файлове и медия не са разрешени"; + /* No comment provided by engineer. */ "Files and media prohibited!" = "Файловете и медията са забранени!"; @@ -1857,6 +1890,21 @@ /* No comment provided by engineer. */ "For console" = "За конзолата"; +/* chat item action */ +"Forward" = "Препрати"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Препращане и запазване на съобщения"; + +/* No comment provided by engineer. */ +"forwarded" = "препратено"; + +/* No comment provided by engineer. */ +"Forwarded" = "Препратено"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Препратено от"; + /* No comment provided by engineer. */ "Found desktop" = "Намерено настолно устройство"; @@ -1935,6 +1983,9 @@ /* No comment provided by engineer. */ "Group members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; +/* No comment provided by engineer. */ +"Group members can send SimpleX links." = "Членовете на групата могат да изпращат SimpleX линкове."; + /* No comment provided by engineer. */ "Group members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; @@ -2076,6 +2127,9 @@ /* No comment provided by engineer. */ "In reply to" = "В отговор на"; +/* No comment provided by engineer. */ +"In-call sounds" = "Звуци по време на разговор"; + /* No comment provided by engineer. */ "Incognito" = "Инкогнито"; @@ -2421,6 +2475,9 @@ /* notification */ "message received" = "получено съобщение"; +/* No comment provided by engineer. */ +"Message source remains private." = "Източникът на съобщението остава скрит."; + /* No comment provided by engineer. */ "Message text" = "Текст на съобщението"; @@ -2436,6 +2493,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Съобщенията от %@ ще бъдат показани!"; +/* No comment provided by engineer. */ +"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом."; + +/* No comment provided by engineer. */ +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Съобщенията, файловете и разговорите са защитени чрез **квантово устойчиво e2e криптиране** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом."; + /* No comment provided by engineer. */ "Migrate device" = "Мигрирай устройството"; @@ -2499,6 +2562,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Очаквайте скоро още подобрения!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "По-надеждна мрежова връзка."; + /* item status description */ "Most likely this connection is deleted." = "Най-вероятно тази връзка е изтрита."; @@ -2520,6 +2586,12 @@ /* No comment provided by engineer. */ "Network & servers" = "Мрежа и сървъри"; +/* No comment provided by engineer. */ +"Network connection" = "Мрежова връзка"; + +/* No comment provided by engineer. */ +"Network management" = "Управление на мрежата"; + /* No comment provided by engineer. */ "Network settings" = "Мрежови настройки"; @@ -2598,6 +2670,9 @@ /* No comment provided by engineer. */ "No history" = "Няма история"; +/* No comment provided by engineer. */ +"No network connection" = "Няма мрежова връзка"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Няма разрешение за запис на гласово съобщение"; @@ -2744,9 +2819,15 @@ /* No comment provided by engineer. */ "Or show this code" = "Или покажи този код"; +/* No comment provided by engineer. */ +"Other" = "Други"; + /* member role */ "owner" = "собственик"; +/* feature role */ +"owners" = "собственици"; + /* No comment provided by engineer. */ "Passcode" = "Код за достъп"; @@ -2849,9 +2930,6 @@ /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; -/* No comment provided by engineer. */ -"Post-quantum E2EE" = "Постквантово E2EE"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Запазете последната чернова на съобщението с прикачени файлове."; @@ -2882,6 +2960,9 @@ /* No comment provided by engineer. */ "Profile image" = "Профилно изображение"; +/* No comment provided by engineer. */ +"Profile images" = "Профилни изображения"; + /* No comment provided by engineer. */ "Profile name" = "Име на профила"; @@ -2915,6 +2996,9 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "Забрани изпращането на файлове и медия."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Забранете изпращането на SimpleX линкове."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Забрани изпращането на гласови съобщения."; @@ -2993,6 +3077,9 @@ /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн."; +/* No comment provided by engineer. */ +"Receiving concurrency" = "Паралелност на получаване"; + /* No comment provided by engineer. */ "Receiving file will be stopped." = "Получаващият се файл ще бъде спрян."; @@ -3002,6 +3089,9 @@ /* No comment provided by engineer. */ "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Скорошна история и подобрен [bot за директория за групи](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPd jdLW3%23%2F%3Fv%3D1-2% 26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."; +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Получателят(ите) не могат да видят от кого е това съобщение."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Получателите виждат актуализации, докато ги въвеждате."; @@ -3197,6 +3287,18 @@ /* No comment provided by engineer. */ "Save welcome message?" = "Запази съобщението при посрещане?"; +/* No comment provided by engineer. */ +"saved" = "запазено"; + +/* No comment provided by engineer. */ +"Saved" = "Запазено"; + +/* No comment provided by engineer. */ +"Saved from" = "Запазено от"; + +/* No comment provided by engineer. */ +"saved from %@" = "запазено от %@"; + /* message info title */ "Saved message" = "Запазено съобщение"; @@ -3404,6 +3506,9 @@ /* No comment provided by engineer. */ "Settings" = "Настройки"; +/* No comment provided by engineer. */ +"Shape profile images" = "Променете формата на профилните изображения"; + /* chat item action */ "Share" = "Сподели"; @@ -3461,9 +3566,15 @@ /* simplex link type */ "SimpleX group link" = "SimpleX групов линк"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX линкове"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited in this group." = "SimpleX линкове са забранени в тази група."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "SimpleX линковете не са разрешени"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX заключване"; @@ -3500,6 +3611,9 @@ /* notification title */ "Somebody" = "Някой"; +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "Квадрат, кръг или нещо между тях."; + /* chat item text */ "standard end-to-end encryption" = "стандартно криптиране от край до край"; @@ -4028,6 +4142,9 @@ /* No comment provided by engineer. */ "Voice messages are prohibited in this group." = "Гласовите съобщения са забранени в тази група."; +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Гласовите съобщения не са разрешени"; + /* No comment provided by engineer. */ "Voice messages prohibited!" = "Гласовите съобщения са забранени!"; @@ -4079,12 +4196,24 @@ /* No comment provided by engineer. */ "When available" = "Когато са налични"; +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "При свързване на аудио и видео разговори."; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите."; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Ще бъде активирано в личните чатове!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Кабелен Ethernet"; + /* No comment provided by engineer. */ "With encrypted files and media." = "С криптирани файлове и медия."; @@ -4106,9 +4235,15 @@ /* pref value */ "yes" = "да"; +/* No comment provided by engineer. */ +"you" = "вие"; + /* No comment provided by engineer. */ "You" = "Вие"; +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "**Не трябва** да използвате една и съща база данни на две устройства."; + /* No comment provided by engineer. */ "You accepted connection" = "Вие приехте връзката"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index a777a33613..e2087547bd 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -2903,7 +2903,7 @@ /* simplex link type */ "SimpleX group link" = "Skupinový odkaz SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Odkazy na SimpleX"; /* No comment provided by engineer. */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index e2cfcee022..5fc393fa1f 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -389,6 +389,9 @@ /* member role */ "admin" = "Admin"; +/* feature role */ +"admins" = "Administratoren"; + /* No comment provided by engineer. */ "Admins can block a member for all." = "Administratoren können ein Gruppenmitglied für Alle blockieren."; @@ -416,6 +419,9 @@ /* No comment provided by engineer. */ "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; +/* feature role */ +"all members" = "Alle Mitglieder"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!"; @@ -464,6 +470,9 @@ /* No comment provided by engineer. */ "Allow to send files and media." = "Das Senden von Dateien und Medien erlauben."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Das Senden von SimpleX-Links erlauben."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Das Senden von Sprachnachrichten erlauben."; @@ -707,6 +716,9 @@ /* No comment provided by engineer. */ "Cannot receive file" = "Datei kann nicht empfangen werden"; +/* No comment provided by engineer. */ +"Cellular" = "Zellulär"; + /* No comment provided by engineer. */ "Change" = "Ändern"; @@ -1401,6 +1413,9 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Datenbank herabstufen und den Chat öffnen"; +/* chat item action */ +"Download" = "Herunterladen"; + /* No comment provided by engineer. */ "Download failed" = "Herunterladen fehlgeschlagen"; @@ -1476,6 +1491,9 @@ /* enabled status */ "enabled" = "Aktiviert"; +/* No comment provided by engineer. */ +"Enabled for" = "Aktiviert für"; + /* enabled status */ "enabled for contact" = "Für Kontakt aktiviert"; @@ -1605,9 +1623,6 @@ /* No comment provided by engineer. */ "Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern"; -/* No comment provided by engineer. */ -"Error allowing contact PQ encryption" = "Fehler beim Zulassen der Kontakt-PQ-Verschlüsselung"; - /* No comment provided by engineer. */ "Error changing address" = "Fehler beim Wechseln der Empfängeradresse"; @@ -1833,6 +1848,9 @@ /* No comment provided by engineer. */ "Files and media are prohibited in this group." = "In dieser Gruppe sind Dateien und Medien nicht erlaubt."; +/* No comment provided by engineer. */ +"Files and media not allowed" = "Dateien und Medien sind nicht erlaubt"; + /* No comment provided by engineer. */ "Files and media prohibited!" = "Dateien und Medien sind nicht erlaubt!"; @@ -1872,6 +1890,21 @@ /* No comment provided by engineer. */ "For console" = "Für Konsole"; +/* chat item action */ +"Forward" = "Weiterleiten"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Nachrichten weiterleiten und speichern"; + +/* No comment provided by engineer. */ +"forwarded" = "weitergeleitet"; + +/* No comment provided by engineer. */ +"Forwarded" = "Weitergeleitet"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Weitergeleitet aus"; + /* No comment provided by engineer. */ "Found desktop" = "Gefundener Desktop"; @@ -1950,6 +1983,9 @@ /* No comment provided by engineer. */ "Group members can send files and media." = "Gruppenmitglieder können Dateien und Medien senden."; +/* No comment provided by engineer. */ +"Group members can send SimpleX links." = "Gruppenmitglieder können SimpleX-Links senden."; + /* No comment provided by engineer. */ "Group members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; @@ -2091,6 +2127,9 @@ /* No comment provided by engineer. */ "In reply to" = "Als Antwort auf"; +/* No comment provided by engineer. */ +"In-call sounds" = "Klingeltöne"; + /* No comment provided by engineer. */ "Incognito" = "Inkognito"; @@ -2436,6 +2475,9 @@ /* notification */ "message received" = "Nachricht empfangen"; +/* No comment provided by engineer. */ +"Message source remains private." = "Die Nachrichtenquelle bleibt privat."; + /* No comment provided by engineer. */ "Message text" = "Nachrichtentext"; @@ -2520,6 +2562,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Weitere Verbesserungen sind bald verfügbar!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Zuverlässigere Netzwerkverbindung."; + /* item status description */ "Most likely this connection is deleted." = "Wahrscheinlich ist diese Verbindung gelöscht worden."; @@ -2541,6 +2586,12 @@ /* No comment provided by engineer. */ "Network & servers" = "Netzwerk & Server"; +/* No comment provided by engineer. */ +"Network connection" = "Netzwerkverbindung"; + +/* No comment provided by engineer. */ +"Network management" = "Netzwerk-Verwaltung"; + /* No comment provided by engineer. */ "Network settings" = "Netzwerkeinstellungen"; @@ -2619,6 +2670,9 @@ /* No comment provided by engineer. */ "No history" = "Kein Nachrichtenverlauf"; +/* No comment provided by engineer. */ +"No network connection" = "Keine Netzwerkverbindung"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Keine Berechtigung für das Aufnehmen von Sprachnachrichten"; @@ -2765,9 +2819,15 @@ /* No comment provided by engineer. */ "Or show this code" = "Oder diesen QR-Code anzeigen"; +/* No comment provided by engineer. */ +"Other" = "Andere"; + /* member role */ "owner" = "Eigentümer"; +/* feature role */ +"owners" = "Eigentümer"; + /* No comment provided by engineer. */ "Passcode" = "Zugangscode"; @@ -2870,9 +2930,6 @@ /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig"; -/* No comment provided by engineer. */ -"Post-quantum E2EE" = "Post-Quantum E2E-Verschlüsselung"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren."; @@ -2903,6 +2960,9 @@ /* No comment provided by engineer. */ "Profile image" = "Profilbild"; +/* No comment provided by engineer. */ +"Profile images" = "Profil-Bilder"; + /* No comment provided by engineer. */ "Profile name" = "Profilname"; @@ -2931,11 +2991,14 @@ "Prohibit sending direct messages to members." = "Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben."; /* No comment provided by engineer. */ -"Prohibit sending disappearing messages." = "Das Senden von verschwindenden Nachrichten verbieten."; +"Prohibit sending disappearing messages." = "Das Senden von verschwindenden Nachrichten nicht erlauben."; /* No comment provided by engineer. */ "Prohibit sending files and media." = "Das Senden von Dateien und Medien nicht erlauben."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Das Senden von SimpleX-Links nicht erlauben."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Das Senden von Sprachnachrichten nicht erlauben."; @@ -3014,6 +3077,9 @@ /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist."; +/* No comment provided by engineer. */ +"Receiving concurrency" = "Gleichzeitiger Empfang"; + /* No comment provided by engineer. */ "Receiving file will be stopped." = "Der Empfang der Datei wird beendet."; @@ -3023,6 +3089,9 @@ /* No comment provided by engineer. */ "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."; +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Empfänger können nicht sehen, von wem die Nachricht stammt."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben."; @@ -3218,6 +3287,18 @@ /* No comment provided by engineer. */ "Save welcome message?" = "Begrüßungsmeldung speichern?"; +/* No comment provided by engineer. */ +"saved" = "abgespeichert"; + +/* No comment provided by engineer. */ +"Saved" = "Abgespeichert"; + +/* No comment provided by engineer. */ +"Saved from" = "Abgespeichert von"; + +/* No comment provided by engineer. */ +"saved from %@" = "abgespeichert von %@"; + /* message info title */ "Saved message" = "Gespeicherte Nachricht"; @@ -3425,6 +3506,9 @@ /* No comment provided by engineer. */ "Settings" = "Einstellungen"; +/* No comment provided by engineer. */ +"Shape profile images" = "Form der Profil-Bilder"; + /* chat item action */ "Share" = "Teilen"; @@ -3482,9 +3566,15 @@ /* simplex link type */ "SimpleX group link" = "SimpleX Gruppen-Link"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX-Links"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited in this group." = "In dieser Gruppe sind SimpleX-Links nicht erlaubt."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "SimpleX-Links sind nicht erlaubt"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX-Sperre"; @@ -3521,6 +3611,9 @@ /* notification title */ "Somebody" = "Jemand"; +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "Quadratisch, kreisförmig oder irgendetwas dazwischen."; + /* chat item text */ "standard end-to-end encryption" = "Standard-Ende-zu-Ende-Verschlüsselung"; @@ -3936,7 +4029,7 @@ "Use chat" = "Verwenden Sie Chat"; /* No comment provided by engineer. */ -"Use current profile" = "Nutzen Sie das aktuelle Profil"; +"Use current profile" = "Das aktuelle Profil nutzen"; /* No comment provided by engineer. */ "Use for new connections" = "Für neue Verbindungen nutzen"; @@ -3948,7 +4041,7 @@ "Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; /* No comment provided by engineer. */ -"Use new incognito profile" = "Nutzen Sie das neue Inkognito-Profil"; +"Use new incognito profile" = "Ein neues Inkognito-Profil nutzen"; /* No comment provided by engineer. */ "Use only local notifications?" = "Nur lokale Benachrichtigungen nutzen?"; @@ -4049,6 +4142,9 @@ /* No comment provided by engineer. */ "Voice messages are prohibited in this group." = "In dieser Gruppe sind Sprachnachrichten nicht erlaubt."; +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Sprachnachrichten sind nicht erlaubt"; + /* No comment provided by engineer. */ "Voice messages prohibited!" = "Sprachnachrichten sind nicht erlaubt!"; @@ -4100,12 +4196,24 @@ /* No comment provided by engineer. */ "When available" = "Wenn verfügbar"; +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "Bei der Verbindung über Audio- und Video-Anrufe."; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen."; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Wird in direkten Chats automatisch aktiviert!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Kabelgebundenes Netzwerk"; + /* No comment provided by engineer. */ "With encrypted files and media." = "Mit verschlüsselten Dateien und Medien."; @@ -4127,6 +4235,9 @@ /* pref value */ "yes" = "Ja"; +/* No comment provided by engineer. */ +"you" = "Profil"; + /* No comment provided by engineer. */ "You" = "Profil"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 7e82b77e2e..8a61d6c438 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -86,7 +86,7 @@ "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Más privado**: no se usa el servidor de notificaciones de SimpleX Chat, los mensajes se comprueban periódicamente en segundo plano (dependiendo de la frecuencia con la que utilices la aplicación)."; /* No comment provided by engineer. */ -"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Tenga en cuenta**: usar la misma base de datos en dos dispositivos interrumpirá el descifrado de mensajes de sus conexiones, como protección de seguridad."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Recuarda**: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Atención**: NO podrás recuperar o cambiar la contraseña si la pierdes."; @@ -143,7 +143,7 @@ "%@ connected" = "%@ conectado"; /* No comment provided by engineer. */ -"%@ downloaded" = "%@ downloaded"; +"%@ downloaded" = "%@ descargado"; /* notification title */ "%@ is connected!" = "%@ ¡está conectado!"; @@ -158,7 +158,7 @@ "%@ servers" = "Servidores %@"; /* No comment provided by engineer. */ -"%@ uploaded" = "%@ cargado"; +"%@ uploaded" = "%@ subido"; /* notification title */ "%@ wants to connect!" = "¡ %@ quiere contactar!"; @@ -389,8 +389,11 @@ /* member role */ "admin" = "administrador"; +/* feature role */ +"admins" = "administradores"; + /* No comment provided by engineer. */ -"Admins can block a member for all." = "Los admins pueden bloquear un miembro por todos."; +"Admins can block a member for all." = "Los administradores pueden bloquear a un miembro para los demás."; /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Los administradores pueden crear enlaces para unirse a grupos."; @@ -416,6 +419,9 @@ /* No comment provided by engineer. */ "All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; +/* feature role */ +"all members" = "todos los miembros"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Todos los mensajes serán borrados. ¡No podrá deshacerse!"; @@ -432,7 +438,7 @@ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Todos tus contactos permanecerán conectados. La actualización del perfil se enviará a tus contactos."; /* No comment provided by engineer. */ -"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Todos sus contactos, conversaciones y archivos se cifrarán de forma segura y se subirán en fragmentos hacia puntos XFTP configurados."; +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Todos tus contactos, conversaciones y archivos serán cifrados, divididos y subidos de forma segura a los servidores XFTP configurados."; /* No comment provided by engineer. */ "Allow" = "Se permite"; @@ -464,6 +470,9 @@ /* No comment provided by engineer. */ "Allow to send files and media." = "Se permite enviar archivos y multimedia."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Permitir enviar enlaces SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Permites enviar mensajes de voz."; @@ -516,7 +525,7 @@ "App build: %@" = "Compilación app: %@"; /* No comment provided by engineer. */ -"App data migration" = "Migración de datos de la aplicación"; +"App data migration" = "Migrar datos de la aplicación"; /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Cifrado de los nuevos archivos locales (excepto vídeos)."; @@ -543,7 +552,7 @@ "Apply" = "Aplicar"; /* No comment provided by engineer. */ -"Archive and upload" = "Archivar y transferir"; +"Archive and upload" = "Archivar y subir"; /* No comment provided by engineer. */ "Archiving database" = "Archivando base de datos"; @@ -636,13 +645,13 @@ "blocked" = "bloqueado"; /* rcv group event chat item */ -"blocked %@" = "%@ bloqueado"; +"blocked %@" = "ha bloqueado a %@"; /* marked deleted chat item preview text */ -"blocked by admin" = "bloqueado por el administrador"; +"blocked by admin" = "bloqueado por administrador"; /* No comment provided by engineer. */ -"Blocked by admin" = "Bloqueado por el administrador"; +"Blocked by admin" = "Bloqueado por administrador"; /* No comment provided by engineer. */ "bold" = "negrita"; @@ -707,6 +716,9 @@ /* No comment provided by engineer. */ "Cannot receive file" = "No se puede recibir el archivo"; +/* No comment provided by engineer. */ +"Cellular" = "Móvil"; + /* No comment provided by engineer. */ "Change" = "Cambiar"; @@ -772,13 +784,13 @@ "Chat is running" = "Chat está en ejecución"; /* No comment provided by engineer. */ -"Chat is stopped" = "Chat está detenido"; +"Chat is stopped" = "Chat está parado"; /* No comment provided by engineer. */ -"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Chat está detenido. Si estás usando esta base de datos en otro dispositivo, deberías transferirla de vuelta antes de iniciarlo."; +"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar Chat."; /* No comment provided by engineer. */ -"Chat migrated!" = "Chat transferido !"; +"Chat migrated!" = "¡Chat migrado!"; /* No comment provided by engineer. */ "Chat preferences" = "Preferencias de Chat"; @@ -793,7 +805,7 @@ "Chinese and Spanish interface" = "Interfaz en chino y español"; /* No comment provided by engineer. */ -"Choose _Migrate from another device_ on the new device and scan QR code." = "Elija _Migrar desde otro dispositivo_ en el nuevo dispositivo y escanee el código QR."; +"Choose _Migrate from another device_ on the new device and scan QR code." = "En el nuevo dispositivo selecciona _Migrar desde otro dispositivo_ y escanéa el código QR."; /* No comment provided by engineer. */ "Choose file" = "Elije archivo"; @@ -841,7 +853,7 @@ "Confirm database upgrades" = "Confirmar actualizaciones de la bases de datos"; /* No comment provided by engineer. */ -"Confirm network settings" = "Confirmar los ajustes de red"; +"Confirm network settings" = "Confirmar configuración de red"; /* No comment provided by engineer. */ "Confirm new passphrase…" = "Confirme nueva contraseña…"; @@ -853,7 +865,7 @@ "Confirm password" = "Confirmar contraseña"; /* No comment provided by engineer. */ -"Confirm that you remember database passphrase to migrate it." = "Confirme que recuerda la frase secreta de la base de datos para migrarla."; +"Confirm that you remember database passphrase to migrate it." = "Confirma que recuerdas la frase de contraseña de la base de datos para migrarla."; /* No comment provided by engineer. */ "Confirm upload" = "Confirmar subida"; @@ -1057,7 +1069,7 @@ "Created on %@" = "Creado en %@"; /* No comment provided by engineer. */ -"Creating archive link" = "Creando enlace de archivo"; +"Creating archive link" = "Creando enlace al archivo"; /* No comment provided by engineer. */ "Creating link…" = "Creando enlace…"; @@ -1189,7 +1201,7 @@ "Delete chat profile" = "Eliminar perfil"; /* No comment provided by engineer. */ -"Delete chat profile?" = "¿Eliminar el perfil?"; +"Delete chat profile?" = "¿Eliminar perfil?"; /* No comment provided by engineer. */ "Delete connection" = "Eliminar conexión"; @@ -1401,8 +1413,11 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Degradar y abrir Chat"; +/* chat item action */ +"Download" = "Descargar"; + /* No comment provided by engineer. */ -"Download failed" = "Error en la descarga"; +"Download failed" = "Descarga fallida"; /* server test step */ "Download file" = "Descargar archivo"; @@ -1447,7 +1462,7 @@ "Enable for all" = "Activar para todos"; /* No comment provided by engineer. */ -"Enable in direct chats (BETA)!" = "Activar en chats directos (BETA)!"; +"Enable in direct chats (BETA)!" = "¡Activar en chats directos (BETA)!"; /* No comment provided by engineer. */ "Enable instant notifications?" = "¿Activar notificación instantánea?"; @@ -1476,6 +1491,9 @@ /* enabled status */ "enabled" = "activado"; +/* No comment provided by engineer. */ +"Enabled for" = "Activar para"; + /* enabled status */ "enabled for contact" = "activado para el contacto"; @@ -1564,7 +1582,7 @@ "Enter Passcode" = "Introduce Código"; /* No comment provided by engineer. */ -"Enter passphrase" = "Introducir frase de contraseña"; +"Enter passphrase" = "Introduce la frase de contraseña"; /* No comment provided by engineer. */ "Enter passphrase…" = "Introduce la contraseña…"; @@ -1605,9 +1623,6 @@ /* No comment provided by engineer. */ "Error adding member(s)" = "Error al añadir miembro(s)"; -/* No comment provided by engineer. */ -"Error allowing contact PQ encryption" = "Error al permitir cifrado PQ al contacto"; - /* No comment provided by engineer. */ "Error changing address" = "Error al cambiar servidor"; @@ -1735,7 +1750,7 @@ "Error starting chat" = "Error al iniciar Chat"; /* No comment provided by engineer. */ -"Error stopping chat" = "Error al detener Chat"; +"Error stopping chat" = "Error al parar Chat"; /* No comment provided by engineer. */ "Error switching profile!" = "¡Error al cambiar perfil!"; @@ -1816,10 +1831,10 @@ "File will be deleted from servers." = "El archivo será eliminado de los servidores."; /* No comment provided by engineer. */ -"File will be received when your contact completes uploading it." = "El archivo se recibirá cuando tu contacto termine de subirlo."; +"File will be received when your contact completes uploading it." = "El archivo se recibirá cuando el contacto termine de subirlo."; /* No comment provided by engineer. */ -"File will be received when your contact is online, please wait or check later!" = "El archivo se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; +"File will be received when your contact is online, please wait or check later!" = "El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; /* No comment provided by engineer. */ "File: %@" = "Archivo: %@"; @@ -1831,7 +1846,10 @@ "Files and media" = "Archivos y multimedia"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "No se permiten archivos y multimedia en este grupo."; +"Files and media are prohibited in this group." = "Los archivos y multimedia no están permitidos en este grupo."; + +/* No comment provided by engineer. */ +"Files and media not allowed" = "Archivos y multimedia no permitidos"; /* No comment provided by engineer. */ "Files and media prohibited!" = "¡Archivos y multimedia no permitidos!"; @@ -1840,7 +1858,7 @@ "Filter unread and favorite chats." = "Filtra chats no leídos y favoritos."; /* No comment provided by engineer. */ -"Finalize migration" = "Finalizar la migración"; +"Finalize migration" = "Finalizar migración"; /* No comment provided by engineer. */ "Finalize migration on another device." = "Finalizar la migración en otro dispositivo."; @@ -1872,6 +1890,21 @@ /* No comment provided by engineer. */ "For console" = "Para consola"; +/* chat item action */ +"Forward" = "Reenviar"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Reenviar y guardar mensajes"; + +/* No comment provided by engineer. */ +"forwarded" = "reenviado"; + +/* No comment provided by engineer. */ +"Forwarded" = "Reenviado"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Reenviado por"; + /* No comment provided by engineer. */ "Found desktop" = "Ordenador encontrado"; @@ -1950,6 +1983,9 @@ /* No comment provided by engineer. */ "Group members can send files and media." = "Los miembros del grupo pueden enviar archivos y multimedia."; +/* No comment provided by engineer. */ +"Group members can send SimpleX links." = "Los miembros del grupo pueden enviar enlaces SimpleX."; + /* No comment provided by engineer. */ "Group members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; @@ -2029,7 +2065,7 @@ "How to use your servers" = "Cómo usar los servidores"; /* No comment provided by engineer. */ -"Hungarian interface" = "Interfaz húngara"; +"Hungarian interface" = "Interfaz en húngaro"; /* No comment provided by engineer. */ "ICE servers (one per line)" = "Servidores ICE (uno por línea)"; @@ -2050,10 +2086,10 @@ "Ignore" = "Ignorar"; /* No comment provided by engineer. */ -"Image will be received when your contact completes uploading it." = "La imagen se recibirá cuando tu contacto termine de subirla."; +"Image will be received when your contact completes uploading it." = "La imagen se recibirá cuando el contacto termine de subirla."; /* No comment provided by engineer. */ -"Image will be received when your contact is online, please wait or check later!" = "La imagen se recibirá cuando tu contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; +"Image will be received when your contact is online, please wait or check later!" = "La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; /* No comment provided by engineer. */ "Immediately" = "Inmediatamente"; @@ -2086,11 +2122,14 @@ "Improved server configuration" = "Configuración del servidor mejorada"; /* No comment provided by engineer. */ -"In order to continue, chat should be stopped." = "Para continuar, el chat debe ser interrumpido."; +"In order to continue, chat should be stopped." = "Para continuar, Chat debe estar parado."; /* No comment provided by engineer. */ "In reply to" = "En respuesta a"; +/* No comment provided by engineer. */ +"In-call sounds" = "Sonido de llamada"; + /* No comment provided by engineer. */ "Incognito" = "Incógnito"; @@ -2173,7 +2212,7 @@ "Invalid link" = "Enlace no válido"; /* No comment provided by engineer. */ -"Invalid migration confirmation" = "Confirmación de migración inválida"; +"Invalid migration confirmation" = "Confirmación de migración no válida"; /* No comment provided by engineer. */ "Invalid name!" = "¡Nombre no válido!"; @@ -2239,7 +2278,7 @@ "It can happen when you or your connection used the old database backup." = "Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos."; /* No comment provided by engineer. */ -"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Esto puede suceder cuando:\n1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días.\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.\n3. La conexión ha sido comprometida."; +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Esto puede suceder cuando:\n1. Los mensajes caducan tras 2 días en el cliente saliente o tras 30 días en el servidor.\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.\n3. La conexión ha sido comprometida."; /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Parece que ya estás conectado mediante este enlace. Si no es así ha habido un error (%@)."; @@ -2436,11 +2475,14 @@ /* notification */ "message received" = "mensaje recibido"; +/* No comment provided by engineer. */ +"Message source remains private." = "El autor del mensaje se mantiene privado."; + /* No comment provided by engineer. */ "Message text" = "Contacto y texto"; /* No comment provided by engineer. */ -"Message too large" = "Mensaje demasiado grande"; +"Message too large" = "Mensaje demasiado largo"; /* No comment provided by engineer. */ "Messages" = "Mensajes"; @@ -2452,10 +2494,10 @@ "Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Los mensajes, archivos y llamadas están protegidos por **cifrado de extremo a extremo** con perfecta confidencialidad, repudio y recuperación tras ataques."; +"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque."; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Los mensajes, archivos y llamadas están protegidos por **cifrado de extremo a extremo resistente a la computación cuántica** con perfecta confidencialidad, repudio y recuperación tras ataques."; +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo resistente a tecnología cuántica** con secreto perfecto hacía adelante, repudio y recuperación tras ataque."; /* No comment provided by engineer. */ "Migrate device" = "Migrar dispositivo"; @@ -2467,10 +2509,10 @@ "Migrate here" = "Migrar aquí"; /* No comment provided by engineer. */ -"Migrate to another device" = "Migrar hacia otro dispositivo"; +"Migrate to another device" = "Migrar a otro dispositivo"; /* No comment provided by engineer. */ -"Migrate to another device via QR code." = "Migrar hacia otro dispositivo mediante código QR."; +"Migrate to another device via QR code." = "Migrar a otro dispositivo mediante código QR."; /* No comment provided by engineer. */ "Migrating" = "Migrando"; @@ -2520,6 +2562,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "¡Pronto habrá más mejoras!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Conexión de red más fiable."; + /* item status description */ "Most likely this connection is deleted." = "Probablemente la conexión ha sido eliminada."; @@ -2541,6 +2586,12 @@ /* No comment provided by engineer. */ "Network & servers" = "Servidores y Redes"; +/* No comment provided by engineer. */ +"Network connection" = "Conexión de red"; + +/* No comment provided by engineer. */ +"Network management" = "Gestión de la red"; + /* No comment provided by engineer. */ "Network settings" = "Configuración de red"; @@ -2619,6 +2670,9 @@ /* No comment provided by engineer. */ "No history" = "Sin historial"; +/* No comment provided by engineer. */ +"No network connection" = "Sin conexión de red"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Sin permiso para grabar mensajes de voz"; @@ -2739,7 +2793,7 @@ "Open group" = "Grupo abierto"; /* authentication reason */ -"Open migration to another device" = "Abrir la migración hacia otro dispositivo"; +"Open migration to another device" = "Abrir menú migración a otro dispositivo"; /* No comment provided by engineer. */ "Open Settings" = "Abrir Configuración"; @@ -2760,14 +2814,20 @@ "Or scan QR code" = "O escanear código QR"; /* No comment provided by engineer. */ -"Or securely share this file link" = "O comparta de forma segura el enlace de este archivo"; +"Or securely share this file link" = "O comparte de forma segura este enlace al archivo"; /* No comment provided by engineer. */ "Or show this code" = "O mostrar este código"; +/* No comment provided by engineer. */ +"Other" = "Otro"; + /* member role */ "owner" = "propietario"; +/* feature role */ +"owners" = "propietarios"; + /* No comment provided by engineer. */ "Passcode" = "Código de acceso"; @@ -2835,7 +2895,7 @@ "Please check yours and your contact preferences." = "Comprueba tus preferencias y las de tu contacto."; /* No comment provided by engineer. */ -"Please confirm that network settings are correct for this device." = "Por favor confirme que la configuración de red es correcta para este dispositivo."; +"Please confirm that network settings are correct for this device." = "Por favor, confirma que la configuración de red es correcta para este dispositivo."; /* No comment provided by engineer. */ "Please contact developers.\nError: %@" = "Por favor, contacta con los desarrolladores.\nError: %@"; @@ -2870,9 +2930,6 @@ /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta"; -/* No comment provided by engineer. */ -"Post-quantum E2EE" = "E2EE postcuántica"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva el último borrador del mensaje con los datos adjuntos."; @@ -2898,11 +2955,14 @@ "Private notes" = "Notas privadas"; /* No comment provided by engineer. */ -"Profile and server connections" = "Perfil y conexiones de servidor"; +"Profile and server connections" = "Datos del perfil y conexiones"; /* No comment provided by engineer. */ "Profile image" = "Imagen del perfil"; +/* No comment provided by engineer. */ +"Profile images" = "Imágenes del perfil"; + /* No comment provided by engineer. */ "Profile name" = "Nombre del perfil"; @@ -2936,6 +2996,9 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "No permitir el envío de archivos y multimedia."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "No permitir el envío de enlaces SimpleX."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "No se permiten mensajes de voz."; @@ -2958,10 +3021,10 @@ "Push server" = "Servidor push"; /* chat item text */ -"quantum resistant e2e encryption" = "cifrado e2e resistente a la cuántica"; +"quantum resistant e2e encryption" = "cifrado e2e resistente a tecnología cuántica"; /* No comment provided by engineer. */ -"Quantum resistant encryption" = "Cifrado resistente a la tecnología cuántica"; +"Quantum resistant encryption" = "Cifrado resistente a tecnología cuántica"; /* No comment provided by engineer. */ "Rate the app" = "Valora la aplicación"; @@ -3014,6 +3077,9 @@ /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea."; +/* No comment provided by engineer. */ +"Receiving concurrency" = "Concurrencia en la recepción"; + /* No comment provided by engineer. */ "Receiving file will be stopped." = "Se detendrá la recepción del archivo."; @@ -3023,6 +3089,9 @@ /* No comment provided by engineer. */ "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Historial reciente y [bot del directorio](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) mejorados."; +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Los destinatarios no ven de quién procede este mensaje."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Los destinatarios ven actualizarse mientras escribes."; @@ -3108,7 +3177,7 @@ "Repeat join request?" = "¿Repetir solicitud de admisión?"; /* No comment provided by engineer. */ -"Repeat upload" = "Repetir la carga"; +"Repeat upload" = "Repetir subida"; /* chat item action */ "Reply" = "Responder"; @@ -3218,6 +3287,18 @@ /* No comment provided by engineer. */ "Save welcome message?" = "¿Guardar mensaje de bienvenida?"; +/* No comment provided by engineer. */ +"saved" = "guardado"; + +/* No comment provided by engineer. */ +"Saved" = "Guardado"; + +/* No comment provided by engineer. */ +"Saved from" = "Guardado desde"; + +/* No comment provided by engineer. */ +"saved from %@" = "Guardado desde %@"; + /* message info title */ "Saved message" = "Mensaje guardado"; @@ -3288,7 +3369,7 @@ "Send" = "Enviar"; /* No comment provided by engineer. */ -"Send a live message - it will update for the recipient(s) as you type it" = "Envía un mensaje en vivo: se actualizará para el(los) destinatario(s) a medida que se escribe"; +"Send a live message - it will update for the recipient(s) as you type it" = "Envía un mensaje en vivo: se actualizará para el (los) destinatario(s) a medida que se escribe"; /* No comment provided by engineer. */ "Send delivery receipts to" = "Enviar confirmaciones de entrega a"; @@ -3425,6 +3506,9 @@ /* No comment provided by engineer. */ "Settings" = "Configuración"; +/* No comment provided by engineer. */ +"Shape profile images" = "Dar forma a las imágenes de perfil"; + /* chat item action */ "Share" = "Compartir"; @@ -3482,9 +3566,15 @@ /* simplex link type */ "SimpleX group link" = "Enlace de grupo SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Enlaces SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited in this group." = "Los enlaces SimpleX no se permiten en este grupo."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Enlaces SimpleX no permitidos"; + /* No comment provided by engineer. */ "SimpleX Lock" = "Bloqueo SimpleX"; @@ -3521,6 +3611,9 @@ /* notification title */ "Somebody" = "Alguien"; +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "Cuadrada, circular o cualquier forma intermedia."; + /* chat item text */ "standard end-to-end encryption" = "cifrado estándar de extremo a extremo"; @@ -3537,19 +3630,19 @@ "starting…" = "inicializando…"; /* No comment provided by engineer. */ -"Stop" = "Detener"; +"Stop" = "Parar"; /* No comment provided by engineer. */ -"Stop chat" = "Detener el chat"; +"Stop chat" = "Parar chat"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Detén SimpleX para habilitar las acciones sobre la base de datos"; +"Stop chat to enable database actions" = "Para habilitar las acciones sobre la base de datos, debes parar Chat"; /* No comment provided by engineer. */ -"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes detener Chat. Durante el tiempo que esté detenido no podrás recibir ni enviar mensajes."; +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes parar Chat. Mientras tanto no podrás recibir ni enviar mensajes."; /* No comment provided by engineer. */ -"Stop chat?" = "¿Detener Chat?"; +"Stop chat?" = "¿Parar Chat?"; /* cancel file action */ "Stop file" = "Detener archivo"; @@ -3567,10 +3660,10 @@ "Stop sharing address?" = "¿Dejar de compartir la dirección?"; /* authentication reason */ -"Stop SimpleX" = "Detener SimpleX"; +"Stop SimpleX" = "Parar SimpleX"; /* No comment provided by engineer. */ -"Stopping chat" = "Detención del chat"; +"Stopping chat" = "Parando chat"; /* No comment provided by engineer. */ "strike" = "tachado"; @@ -3642,16 +3735,16 @@ "Thank you for installing SimpleX Chat!" = "¡Gracias por instalar SimpleX Chat!"; /* No comment provided by engineer. */ -"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Gracias a los usuarios: [contribuye vía Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)!"; +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "¡Nuestro agradecimiento a todos los colaboradores, [puedes contribuir a través de Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#traducir-el-aplicaciones)!"; /* No comment provided by engineer. */ -"Thanks to the users – contribute via Weblate!" = "¡Gracias a los colaboradores! Contribuye a través de Weblate."; +"Thanks to the users – contribute via Weblate!" = "¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate"; /* No comment provided by engineer. */ "The 1st platform without any user identifiers – private by design." = "La primera plataforma sin identificadores de usuario: diseñada para la privacidad."; /* No comment provided by engineer. */ -"The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: abre la configuración para habilitar."; +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo."; /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "El intento de cambiar la contraseña de la base de datos no se ha completado."; @@ -3717,7 +3810,7 @@ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán."; /* No comment provided by engineer. */ -"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos."; +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Esta acción es irreversible. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Podría tardar varios minutos."; /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente."; @@ -3726,7 +3819,7 @@ "This chat is protected by end-to-end encryption." = "Este chat está protegido por cifrado de extremo a extremo."; /* E2EE info chat item */ -"This chat is protected by quantum resistant end-to-end encryption." = "Este chat está protegido por un cifrado de extremo a extremo resistente a tecnologías cuánticas."; +"This chat is protected by quantum resistant end-to-end encryption." = "Este chat está protegido por cifrado de extremo a extremo resistente a tecnología cuántica."; /* notification title */ "this contact" = "este contacto"; @@ -3825,7 +3918,7 @@ "Unblock member?" = "¿Desbloquear miembro?"; /* rcv group event chat item */ -"unblocked %@" = "%@ desbloqueado"; +"unblocked %@" = "ha desbloqueado a %@"; /* item status description */ "Unexpected error: %@" = "Error inesperado: %@"; @@ -3921,13 +4014,13 @@ "Upgrade and open chat" = "Actualizar y abrir Chat"; /* No comment provided by engineer. */ -"Upload failed" = "Error de carga"; +"Upload failed" = "Error de subida"; /* server test step */ "Upload file" = "Subir archivo"; /* No comment provided by engineer. */ -"Uploading archive" = "Subiendo el archivo"; +"Uploading archive" = "Subiendo archivo"; /* No comment provided by engineer. */ "Use .onion hosts" = "Usar hosts .onion"; @@ -3960,7 +4053,7 @@ "Use SimpleX Chat servers?" = "¿Usar servidores SimpleX Chat?"; /* No comment provided by engineer. */ -"Use the app while in the call." = "Usar la app durante la llamada."; +"Use the app while in the call." = "Usar la aplicación durante la llamada."; /* No comment provided by engineer. */ "User profile" = "Perfil de usuario"; @@ -4023,10 +4116,10 @@ "video call (not e2e encrypted)" = "videollamada (sin cifrar)"; /* No comment provided by engineer. */ -"Video will be received when your contact completes uploading it." = "El video se recibirá cuando tu contacto termine de subirlo."; +"Video will be received when your contact completes uploading it." = "El video se recibirá cuando el contacto termine de subirlo."; /* No comment provided by engineer. */ -"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde."; +"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde."; /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Vídeos y archivos de hasta 1Gb"; @@ -4049,6 +4142,9 @@ /* No comment provided by engineer. */ "Voice messages are prohibited in this group." = "Los mensajes de voz no están permitidos en este grupo."; +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Mensajes de voz no permitidos"; + /* No comment provided by engineer. */ "Voice messages prohibited!" = "¡Mensajes de voz no permitidos!"; @@ -4074,7 +4170,7 @@ "wants to connect to you!" = "¡quiere contactar contigo!"; /* No comment provided by engineer. */ -"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Advertencia: el inicio del chat en varios dispositivos no es compatible y provocará fallos en la entrega de mensajes"; +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Atención: el inicio del chat en varios dispositivos es incompatible y provocará fallos en la entrega de mensajes"; /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Atención: ¡puedes perder algunos datos!"; @@ -4092,7 +4188,7 @@ "Welcome message" = "Mensaje de bienvenida"; /* No comment provided by engineer. */ -"Welcome message is too long" = "El mensaje de bienvenida es demasiado largo"; +"Welcome message is too long" = "Mensaje de bienvenida demasiado largo"; /* No comment provided by engineer. */ "What's new" = "Novedades"; @@ -4100,12 +4196,24 @@ /* No comment provided by engineer. */ "When available" = "Si disponibles"; +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "Al iniciar llamadas de audio y vídeo."; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud."; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "¡Será habilitado en los chats directos!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Ethernet por cable"; + /* No comment provided by engineer. */ "With encrypted files and media." = "Con cifrado de archivos y multimedia."; @@ -4127,11 +4235,14 @@ /* pref value */ "yes" = "sí"; +/* No comment provided by engineer. */ +"you" = "tu"; + /* No comment provided by engineer. */ "You" = "Tú"; /* No comment provided by engineer. */ -"You **must not** use the same database on two devices." = "**No debe** usar la misma base de datos en dos dispositivos."; +"You **must not** use the same database on two devices." = "**No debes** usar la misma base de datos en dos dispositivos."; /* No comment provided by engineer. */ "You accepted connection" = "Has aceptado la conexión"; @@ -4194,7 +4305,7 @@ "You can enable them later via app Privacy & Security settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad."; /* No comment provided by engineer. */ -"You can give another try." = "Puede intentarlo de nuevo."; +"You can give another try." = "Puedes intentarlo de nuevo."; /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Puedes ocultar o silenciar un perfil deslizándolo a la derecha."; @@ -4308,7 +4419,7 @@ "You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde."; /* No comment provided by engineer. */ -"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde."; +"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o compruébalo más tarde."; /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano."; @@ -4350,7 +4461,7 @@ "Your chat profiles" = "Mis perfiles"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Tu contacto debe estar en línea para que se complete la conexión.\nPuedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo)."; +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "El contacto debe estar en línea para completar la conexión.\nPuedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace nuevo)."; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "El contacto ha enviado un archivo mayor al máximo admitido (%@)."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index ddd085141b..c71f9e089c 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -2867,7 +2867,7 @@ /* simplex link type */ "SimpleX group link" = "SimpleX-ryhmän linkki"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "SimpleX-linkit"; /* No comment provided by engineer. */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index cbc72479e3..5e6c9c1b40 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -389,6 +389,9 @@ /* member role */ "admin" = "admin"; +/* feature role */ +"admins" = "admins"; + /* No comment provided by engineer. */ "Admins can block a member for all." = "Les admins peuvent bloquer un membre pour tous."; @@ -416,6 +419,9 @@ /* No comment provided by engineer. */ "All group members will remain connected." = "Tous les membres du groupe resteront connectés."; +/* feature role */ +"all members" = "tous les membres"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Tous les messages seront supprimés - il n'est pas possible de revenir en arrière !"; @@ -464,6 +470,9 @@ /* No comment provided by engineer. */ "Allow to send files and media." = "Permet l'envoi de fichiers et de médias."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Autorise l'envoi de liens SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Autoriser l'envoi de messages vocaux."; @@ -707,6 +716,9 @@ /* No comment provided by engineer. */ "Cannot receive file" = "Impossible de recevoir le fichier"; +/* No comment provided by engineer. */ +"Cellular" = "Cellulaire"; + /* No comment provided by engineer. */ "Change" = "Changer"; @@ -1401,6 +1413,9 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Rétrograder et ouvrir le chat"; +/* chat item action */ +"Download" = "Télécharger"; + /* No comment provided by engineer. */ "Download failed" = "Échec du téléchargement"; @@ -1476,6 +1491,9 @@ /* enabled status */ "enabled" = "activé"; +/* No comment provided by engineer. */ +"Enabled for" = "Activé pour"; + /* enabled status */ "enabled for contact" = "activé pour le contact"; @@ -1605,9 +1623,6 @@ /* No comment provided by engineer. */ "Error adding member(s)" = "Erreur lors de l'ajout de membre·s"; -/* No comment provided by engineer. */ -"Error allowing contact PQ encryption" = "Erreur lors de la négociation du chiffrement PQ"; - /* No comment provided by engineer. */ "Error changing address" = "Erreur de changement d'adresse"; @@ -1833,6 +1848,9 @@ /* No comment provided by engineer. */ "Files and media are prohibited in this group." = "Les fichiers et les médias sont interdits dans ce groupe."; +/* No comment provided by engineer. */ +"Files and media not allowed" = "Fichiers et médias non autorisés"; + /* No comment provided by engineer. */ "Files and media prohibited!" = "Fichiers et médias interdits !"; @@ -1872,6 +1890,21 @@ /* No comment provided by engineer. */ "For console" = "Pour la console"; +/* chat item action */ +"Forward" = "Transférer"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Transférer et sauvegarder des messages"; + +/* No comment provided by engineer. */ +"forwarded" = "transféré"; + +/* No comment provided by engineer. */ +"Forwarded" = "Transféré"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Transféré depuis"; + /* No comment provided by engineer. */ "Found desktop" = "Bureau trouvé"; @@ -1950,6 +1983,9 @@ /* No comment provided by engineer. */ "Group members can send files and media." = "Les membres du groupe peuvent envoyer des fichiers et des médias."; +/* No comment provided by engineer. */ +"Group members can send SimpleX links." = "Les membres du groupe peuvent envoyer des liens SimpleX."; + /* No comment provided by engineer. */ "Group members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; @@ -2091,6 +2127,9 @@ /* No comment provided by engineer. */ "In reply to" = "En réponse à"; +/* No comment provided by engineer. */ +"In-call sounds" = "Sons d'appel"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2436,6 +2475,9 @@ /* notification */ "message received" = "message reçu"; +/* No comment provided by engineer. */ +"Message source remains private." = "La source du message reste privée."; + /* No comment provided by engineer. */ "Message text" = "Texte du message"; @@ -2520,6 +2562,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Plus d'améliorations à venir !"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Connexion réseau plus fiable."; + /* item status description */ "Most likely this connection is deleted." = "Connexion probablement supprimée."; @@ -2541,6 +2586,12 @@ /* No comment provided by engineer. */ "Network & servers" = "Réseau et serveurs"; +/* No comment provided by engineer. */ +"Network connection" = "Connexion au réseau"; + +/* No comment provided by engineer. */ +"Network management" = "Gestion du réseau"; + /* No comment provided by engineer. */ "Network settings" = "Paramètres réseau"; @@ -2619,6 +2670,9 @@ /* No comment provided by engineer. */ "No history" = "Aucun historique"; +/* No comment provided by engineer. */ +"No network connection" = "Pas de connexion au réseau"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Pas l'autorisation d'enregistrer un message vocal"; @@ -2765,9 +2819,15 @@ /* No comment provided by engineer. */ "Or show this code" = "Ou présenter ce code"; +/* No comment provided by engineer. */ +"Other" = "Autres"; + /* member role */ "owner" = "propriétaire"; +/* feature role */ +"owners" = "propriétaires"; + /* No comment provided by engineer. */ "Passcode" = "Code d'accès"; @@ -2870,9 +2930,6 @@ /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; -/* No comment provided by engineer. */ -"Post-quantum E2EE" = "E2EE post-quantique"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserver le brouillon du dernier message, avec les pièces jointes."; @@ -2903,6 +2960,9 @@ /* No comment provided by engineer. */ "Profile image" = "Image de profil"; +/* No comment provided by engineer. */ +"Profile images" = "Images de profil"; + /* No comment provided by engineer. */ "Profile name" = "Nom du profil"; @@ -2936,6 +2996,9 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "Interdire l'envoi de fichiers et de médias."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Interdire l'envoi de liens SimpleX."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Interdire l'envoi de messages vocaux."; @@ -3014,6 +3077,9 @@ /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne."; +/* No comment provided by engineer. */ +"Receiving concurrency" = "Réception simultanée"; + /* No comment provided by engineer. */ "Receiving file will be stopped." = "La réception du fichier sera interrompue."; @@ -3023,6 +3089,9 @@ /* No comment provided by engineer. */ "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Historique récent et amélioration du [bot annuaire](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."; +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Le(s) destinataire(s) ne peut(vent) pas voir de qui provient ce message."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez."; @@ -3218,6 +3287,18 @@ /* No comment provided by engineer. */ "Save welcome message?" = "Enregistrer le message d'accueil ?"; +/* No comment provided by engineer. */ +"saved" = "enregistré"; + +/* No comment provided by engineer. */ +"Saved" = "Enregistré"; + +/* No comment provided by engineer. */ +"Saved from" = "Enregistré depuis"; + +/* No comment provided by engineer. */ +"saved from %@" = "enregistré à partir de %@"; + /* message info title */ "Saved message" = "Message enregistré"; @@ -3425,6 +3506,9 @@ /* No comment provided by engineer. */ "Settings" = "Paramètres"; +/* No comment provided by engineer. */ +"Shape profile images" = "Images de profil modelable"; + /* chat item action */ "Share" = "Partager"; @@ -3482,9 +3566,15 @@ /* simplex link type */ "SimpleX group link" = "Lien de groupe SimpleX"; -/* No comment provided by engineer. */ +/* chat feature */ "SimpleX links" = "Liens SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited in this group." = "Les liens SimpleX sont interdits dans ce groupe."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Les liens SimpleX ne sont pas autorisés"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX Lock"; @@ -3521,6 +3611,9 @@ /* notification title */ "Somebody" = "Quelqu'un"; +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "Carré, circulaire, ou toute autre forme intermédiaire."; + /* chat item text */ "standard end-to-end encryption" = "chiffrement de bout en bout standard"; @@ -4049,6 +4142,9 @@ /* No comment provided by engineer. */ "Voice messages are prohibited in this group." = "Les messages vocaux sont interdits dans ce groupe."; +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Les messages vocaux ne sont pas autorisés"; + /* No comment provided by engineer. */ "Voice messages prohibited!" = "Messages vocaux interdits !"; @@ -4100,12 +4196,24 @@ /* No comment provided by engineer. */ "When available" = "Quand disponible"; +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "Lors des appels audio et vidéo."; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Vous pouvez accepter ou refuser les demandes de contacts."; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Lorsque vous partagez un profil incognito avec quelqu'un, ce profil sera utilisé pour les groupes auxquels il vous invite."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Activé dans les discussions directes !"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Ethernet câblé"; + /* No comment provided by engineer. */ "With encrypted files and media." = "Avec les fichiers et les médias chiffrés."; @@ -4127,6 +4235,9 @@ /* pref value */ "yes" = "oui"; +/* No comment provided by engineer. */ +"you" = "vous"; + /* No comment provided by engineer. */ "You" = "Vous"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index f9a470fbbc..b590785606 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -20,7 +20,7 @@ "_italic_" = "\\_dőlt_"; /* No comment provided by engineer. */ -"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- kapcsolódás a [könyvtár szolgáltatáshoz] (simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Ld3%3DWpxkKFeXSPv3pwp %2F%3Fv%3D1-2%26dh %3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6glco6bqjETA)4Beklco6bqj)\n- kézbesítési jelentések (legfeljebb 20 tag).\n- gyorsabb és stabilabb."; +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- kapcsolódás a [könyvtár szolgáltatáshoz](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- kézbesítési jelentések (legfeljebb 20 tag).\n- gyorsabb és stabilabb."; /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabilabb üzenetkézbesítés.\n- valamivel jobb csoportok.\n- és még sok más!"; @@ -50,7 +50,7 @@ "(new)" = "(új)"; /* No comment provided by engineer. */ -"(this device v%@)" = "(ez az eszköz v%@)"; +"(this device v%@)" = "(ez az eszköz: v%@)"; /* No comment provided by engineer. */ ")" = ")"; @@ -65,7 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Csillag a GitHubon](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő csatlakozáshoz."; +"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; /* No comment provided by engineer. */ "**Add new contact**: to create your one-time QR Code for your contact." = "**Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása a kapcsolattartóhoz."; @@ -85,6 +85,9 @@ /* No comment provided by engineer. */ "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Legprivátabb**: ne használja a SimpleX Chat értesítési szervert, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; +/* No comment provided by engineer. */ +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését."; + /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Figyelem**: NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt."; @@ -94,6 +97,9 @@ /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés**: Az azonnali push-értesítésekhez a kulcstárolóban tárolt jelmondat megadása szükséges."; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**Figyelem**: az archívum törlésre kerül."; + /* No comment provided by engineer. */ "*bold*" = "\\*félkövér*"; @@ -104,7 +110,7 @@ "## History" = "## Előzmények"; /* copied message info */ -"## In reply to" = "## Válaszul erre"; +"## In reply to" = "## Válaszul erre:"; /* No comment provided by engineer. */ "#secret#" = "#titkos#"; @@ -128,16 +134,19 @@ "%@ and %@" = "%@ és %@"; /* No comment provided by engineer. */ -"%@ and %@ connected" = "%@ és %@ csatlakozott"; +"%@ and %@ connected" = "%@ és %@ kapcsolódott"; /* copied message info, at Ötletek és javaslatok Figyelmeztetés: néhány adat elveszhet! Koppintson az új csevegés indításához Várakozás a számítógépre… A privát üzenetküldés következő generációja Hálózati beállítások megváltoztatása? - Várakozás a mobiltelefon csatlakozására: + Várakozás a mobiltelefon társítására: Kapcsolat biztonságának ellenőrzése fájlok küldése egyelőre még nem támogatott - Azonosítója erre változott: %s + azonosítója erre változott: %s fájlok fogadása egyelőre még nem támogatott Csoport profil mentése Alaphelyzetbe állítás - Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, hiba lehet – kérjük, jelentse. -\nA csatlakozáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. + Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát. +\nA kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. videóhívás (nem e2e titkosított) Alkalmazás új kapcsolatokhoz Az új üzenetek rendszeresen letöltésre kerülnek az alkalmazás által – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push értesítéseket – az eszközről származó adatok nem kerülnek elküldésre a kiszolgálóknak. Számítógép azonosítójának beillesztése - ismerős azonosítójának hivatkozásán keresztül + kapcsolattartási azonosító-hivatkozáson keresztül SimpleX háttérszolgáltatást használja - az akkumulátor néhány százalékát használja naponta.]]> Az ismerősnek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön. \nMegszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással) A jelszó nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonsági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. - Az ismerősei továbbra is csatlakoztatva maradnak. + Az ismerősei továbbra is kapcsolódva maradnak. A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát Az adatbázis nem működik megfelelően. Koppintson további információért A fájl küldése leállt. - Csatlakozási kísérlet a kapcsolat üzeneteinek fogadására használt kiszolgálóhoz ettől az ismerőstől. + Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. Nem lehetett ellenőrizni; próbálja meg újra. Az üzenet minden tag számára moderáltként lesz megjelölve. Értesítések fogadásához adja meg az adatbázis jelmondatát @@ -1337,28 +1337,28 @@ meghívást kapott a csoportba engedélyezze a SimpleX háttérben történő futását a következő párbeszédpanelen. Ellenkező esetben az értesítések letiltásra kerülnek.]]> A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát - Csatlakozni fog a csoport összes tagjához. + Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót. \nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére. - A videó akkor érkezik meg, amikor az ismerőse elérhető, várjon, vagy ellenőrizze később! + A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! Hálózati kapcsolat ellenőrzése a következővel: %1$s, és próbálja újra. A SimpleX zárolás a Beállításokon keresztül kapcsolható be. Az alkalmazás összeomlott Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat. - A kép nem dekódolható. Kérjük, próbálja ki egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel. + A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel. Érvénytelen fájl elérési útvonalat osztott meg. Jelentse a problémát az alkalmazás fejlesztőinek. Már van egy csevegési profil ugyanezzel a megjelenített névvel. Válasszon egy másik nevet. - Csatlakozási kísérlet a kapcsolat üzeneteinek fogadására használt kiszolgálóhoz ettől az ismerőstől (hiba: %1$s). + Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %1$s). A fájl fogadása leállt. Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani! - A videó akkor érkezik meg, amikor az ismerőse befejezte annak feltöltését. + A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. egyszer használatos hivatkozást osztott meg inkognitóban - Kiszolgálóhoz történő csatlakozás, mely az adott ismerőstől érkező üzenetek fogadására szolgál. + Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. Később engedélyezheti a Beállításokban - Akkor tud csatlakozni a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! - eltérő migráció az appban/adatbázisban: %s / %s - %1$s.]]> + Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! + különböző átköltöztetés az alkalmazásban/adatbázisban: %s / %s + %1$s.]]> Profil felfedése Ez a hivatkozás nem érvényes kapcsolati hivatkozás! A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy szkennelje be) az ismerőse eszközén lévő kódot. @@ -1366,7 +1366,7 @@ Ez a beállítás a jelenlegi csevegési profilban lévő üzenetekre érvényes Meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. Ez a csoport már nem létezik. - Ezen a hivatkozáson keresztül már csatlakozik a csoporthoz. + A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. Meghívást kapott a csoportba Ismerőse a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött. Az ismerősei és az üzenetek (kézbesítés után) nem kerülnek tárolásra a SimpleX kiszolgálókon. @@ -1374,24 +1374,24 @@ Megnyitás alkalmazásban gombra.]]> Csevegési profilja elküldésre kerül \naz ismerőse számára - Egy olyan ismerősét próbálja meghívni, akivel inkognitó profilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban - %1$s csoporthoz.]]> + Egy olyan ismerősét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban + %1$s csoporthoz.]]> Amikor az alkalmazás fut Inkognító profilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében meghívók küldése tiltott Kapcsolat izolációs mód - Akkor lesz csatlakoztatva, ha a csatlakozási kérelme elfogadásra került, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, ha a kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később! A hangüzenetek küldése le van tiltva ebben a csoportban. Alkalmazás akkumulátor használata / Korlátlan módot az alkalmazás beállításaiban.]]> Biztonságos kvantumrezisztens protokollon keresztül. - hangüzenetek 5 percig. -\n- egyedi eltűnési időhatár -\n- előzmény szerkesztése - Használat számítógépről gombra a mobil appban és olvassa be a QR-kódot!]]> - %s at %s - Akkor csatlakozik, amikor az ismerősének az eszköze online lesz, várjon, vagy ellenőrizze később! +\n- egyedi eltűnési időhatár. +\n- előzmény szerkesztése. + Használat számítógépről menüpontot a mobil alkalmazásban és olvassa be a QR-kódot.]]> + %s ekkor: %s + Akkor lesz kapcsolódva, amikor az ismerősének az eszköze online lesz, várjon, vagy ellenőrizze később! Kéretlen üzenetek elrejtése. - Használja az .onion hostokat NEM értékre, ha a SOCKS proxy nem támogatja őket.]]> - Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki csatlakozhat önhöz. + Használja az .onion kiszolgálókat NEM értékre, ha a SOCKS proxy nem támogatja őket.]]> + Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz. Létrehozás később Profilja az eszközön van tárolva és csak az ismerőseivel kerül megosztásra. A SimpleX kiszolgálók nem láthatják a profilját. %s szerepkörét megváltoztatta erre: %s @@ -1402,38 +1402,38 @@ Kapcsolat izolációs mód frissítése? Kapcsolat izolációs mód Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak. - A csevegési adatbázis nem titkosított - állítson be egy jelmondatot annak védelméhez! + A csevegési adatbázis nem titkosított - állítson be egy jelmondatot annak védelméhez. Közvetlen internet kapcsolat használata? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. - A fő csevegési profilja megküldésre kerül a csoporttagok számára. + A fő csevegési profilja megküldésre kerül a csoporttagok számára Később engedélyezheti őket az alkalmazás Adatvédelem és biztonság menüpontban. Rejtett profiljának felfedéséhez írja be a teljes jelszót a Csevegési profilok oldal keresőmezőjébe. A csevegés frissítése és megnyitása Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősei számára. fogadja az üzeneteket, ismerősöket – a kiszolgálók, amelyeket az üzenetküldéshez használ.]]> %1$s csoport tagja.]]> - Azonosítója megváltoztatva + azonosítója megváltoztatva Ismerősei engedélyezhetik a teljes üzenet törlést. A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul - nem az eszközön kerül tárolásra. Ha engedélyezni szeretné, hogy egy mobilalkalmazás csatlakozzon a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, ha engedélyezte azt - Profilja, ismerősök és az elküldött üzenetek az eszközön kerülnek tárolásra. + Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra. Alkalmazás akkumulátor használata / Korlátlan módot az alkalmazás beállításaiban.]]> Ez a karakterlánc nem egy meghívó hivatkozás! Új csevegés kezdése - Már csatlakozik ezen az egyszer használatos hivatkozáson keresztül! + A kapcsolódás már folyamatban van ezen az egyszer használatos hivatkozáson keresztül! Nem veszíti el az ismerőseit, ha később törli az azonosítóját. A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár. kapcsolatba akar lépni veled! - Saját szerepköre erre változott: %s + saját szerepköre erre változott: %s A csevegési szolgáltatás elindítható a Beállítások / Adatbázis menüpontban vagy az alkalmazás újraindításával. Kód ellenőrzése a mobilon Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet az újdonságokról.]]> Opcionális üdvözlő üzenettel. Ismeretlen adatbázis hiba: %s - Elrejthet vagy némíthat egy felhasználói profilt - tartsa lenyomva a menühöz! - Inkognító mód csatlakozáskor - Tor .onion host beállítások frissítése? + Elrejthet vagy némíthat egy felhasználói profilt - tartsa lenyomva a menühöz. + Inkognító mód kapcsolódáskor. + Tor .onion kiszolgálók beállításainak frissítése? Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait. Csatlakozott ehhez a csoporthoz %1$s csoporthoz!]]> @@ -1441,18 +1441,18 @@ Ön irányítja csevegését! Kód ellenőrzése a számítógépen Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak. - Csoporttag részére a csatlakozási kérelem eküldésre kerül. + Csoporttag részére a kapcsolódási kérelem elküldésre kerül. Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. - Már kért egy csatlakozást ezen az azonosítón keresztül! - Megoszthatja ezt a SimpleX azonosítót ismerőseivel, hogy kapcsolatba léphessenek ezzel: %s. - Csatlakozási kérelmek esetében, elfogadhatja vagy elutasíthatja azokat. - Megjelenő üzenetet beállítása új tagok részére! + Már kért egy kapcsolódási kérelmet ezen az azonosítón keresztül! + Megoszthatja ezt a SimpleX azonosítót az ismerőseivel, hogy kapcsolatba léphessenek vele: %s. + Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat. + Megjelenítendő üzenet beállítása az új tagok számára! Köszönet a felhasználóknak - hozzájárulás a Weblaten! A kézbesítési jelentés küldése minden ismerős számára engedélyezésre kerül. Protokoll időkorlát KB-onként Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be. Ez a művelet nem vonható vissza - a kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet. - Profilja csak az ismerősei számára kerül megosztásra. + Profilja csak az ismerőseivel kerül megosztásra. Néhány kiszolgáló megbukott a teszten: Koppintson a csatlakozáshoz Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak. @@ -1460,15 +1460,15 @@ Küldés ezen keresztül: Köszönet a felhasználóknak - hozzájárulás a Weblaten! A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő minden ismerős számára. - Bluetooth támogatás és egyéb fejlesztések. + Bluetooth támogatás és további fejlesztések. Ez a funkció még nem támogatott. Próbálja meg a következő kiadásban. A bejegyzés frissítve: %s Tagok meghívásának kihagyása Ezek felülbírálhatóak az ismerős- és csoportbeállításokban. - Ismerőse NEM fog tudni csatlakozni, akivel megosztotta ezt a hivatkozást! + Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban. \nEz később megváltoztatható. - Koppintson az inkognitómódhoz való csatlakozáshoz + Koppintson az inkognitóban való kapcsolódáshoz Jelmondat beállítása az exportáláshoz Kézbesítési jelentések le vannak tiltva a(z) %d csoportban Néhány nem végzetes hiba történt az importálás során – további részletekért a csevegési konzolban olvashat. @@ -1476,7 +1476,7 @@ Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címét. Rendszerhitelesítés helyetti beállítás. A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be. - A csevegés leállítása a csevegő adatbázis exportálásához, importálásához, vagy törléséhez. A csevegés leállítása alatt nem tud üzeneteket fogadni és küldeni. + A csevegés megállítása a csevegő adatbázis exportálásához, importálásához, vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni. Jelmondat mentése a kulcstárolóba Köszönet a felhasználóknak - hozzájárulás a Weblaten! Jelmondat mentése a beállításokban @@ -1507,12 +1507,12 @@ A jelenlegi csevegési profilhoz tartozó új kapcsolatok kiszolgálói Fogadás ezen keresztül: Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez. - A szerepkör \"%s\"-re fog változni. A tag új meghívót kap. + A tag szerepköre erre fog változni: \"%s\". A tag új meghívót fog kapni. profilkép helyőrző A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet! - Ez a művelet nem vonható vissza - profilok, ismerősök, üzenetek és fájlok visszafordíthatatlanul törlésre kerülnek. + Ez a művelet nem vonható vissza - profiljai, ismerősei, üzenetei és fájljai véglegesen törlésre kerülnek. A bejegyzés frissítve - Felhasználói útmutatóban olvasható.]]> + Használati útmutatóban olvasható.]]> A jelmondat a beállításokban egyszerű szövegként van tárolva. Konzol megjelenítése új ablakban Az előző üzenet hash-e más. @@ -1521,8 +1521,8 @@ GitHub tárolónkban.]]> hiba a tartalom megjelenítése közben hiba az üzenet megjelenítésekor - Láthatóvá teheti SimpleX beli ismerősei számára a Beállításokban. - Legfeljebb az utolsó 100 üzenet kerül elküldésre az új tagoknak. + Láthatóvá teheti SimpleX-beli ismerősei számára a Beállításokban. + Legfeljebb az utolsó 100 üzenet kerül elküldésre az új tagok számára. A beolvasott kód nem egy SimpleX hivatkozás QR-kód. A beillesztett szöveg nem egy SimpleX hivatkozás. A meghívó hivatkozását újra megtekintheti a kapcsolat részleteinél. @@ -1532,14 +1532,14 @@ Ismerős hozzáadása Koppintson a beolvasáshoz Koppintson a hivatkozás beillesztéséhez - Ismerős hozzáadása: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő csatlakozáshoz.]]> + Ismerős hozzáadása: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz.]]> Csoport létrehozása: új csoport létrehozásához.]]> A csevegés leállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés megkezdése előtt. - Az előzmények nem kerülnek elküldésre új tagok részére. + Az előzmények nem kerülnek elküldésre az új tagok számára. Újrapróbálkozás A fényképező nem elérhető - Az utolsó 100 üzenet elküldése az új tagoknak. - Ne küldjön előzményeket az új tagok részére. + Az utolsó 100 üzenet elküldése az új tagok számára. + Az előzmények ne kerüljenek elküldésre az új tagok számára. Vagy mutassa meg ezt a kódot Kamera hozzáférés engedélyezése Fel nem használt meghívó megtartása? @@ -1588,13 +1588,13 @@ %s mobil eszköz elfoglalt]]> Legutóbbi tag %1$s ismeretlen státusz - %1$s tag neve megváltozott erre: %2$s - törölt csatlakozási cím + %1$s megváltoztatta a nevét erre: %2$s + törölt kapcsolattartási azonosító törölt profilkép - új kapcsolattartási cím beállítása + új kapcsolattartási azonosító beállítása új profilkép beállítása frissített profil - %1$s ismerős neve megváltozott erre: %2$s + %1$s megváltoztatta a nevét erre: %2$s Privát jegyzetek Hiba a privát jegyzetek törlésekor Hiba az üzenet létrehozásakor @@ -1602,12 +1602,12 @@ Létrehozva ekkor: Mentett üzenet Megosztva ekkor: %s - Minden üzenet törlésre kerül – ez nem vonható vissza! + Minden üzenet törlésre kerül – ez a művelet nem vonható vissza! Továbbfejlesztett üzenetküldés Csatlakozás csoportos beszélgetésekhez - Hivatkozás beillesztése a csatlakozáshoz! + Hivatkozás beillesztése a kapcsolódáshoz! Privát jegyzetek - A keresősáv fogadja a meghívó hivatkozásokat. + A keresősáv elfogadja a meghívó hivatkozásokat. Titkosított fájlokkal és médiatartalommal. Csökkentett akkumulátorhasználattal. Magyar és török felhasználói felület @@ -1627,7 +1627,7 @@ Hiba a tag mindenki számára való letiltása során Az üzenet túl nagy Az üdvözlő üzenet túl hosszú - Az adatbázis migrációja folyamatban van. + Az adatbázis átköltöztetése folyamatban van. \nEz eltarthat néhány percig. Hanghívás A hívás befejeződött @@ -1641,15 +1641,15 @@ Feltöltés megerősítése Hiba az adatbázis törlésekor Az adminok egy tagot mindenki számára letilthatnak. - Minden ismerős, a beszélgetések és a fájlok biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP átjátszókra. + Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP átjátszókra. Alkalmazásadatok átköltöztetése Adatbázis archiválása - Az átköltöztetés megszakítása + Átköltöztetés visszavonása A csevegés átköltöztetve! Ellenőrizze az internetkapcsolatot, és próbálja újra Archív hivatkozás létrehozása Adatbázis törlése erről az eszközről - Letöltés sikertelen + Sikertelen letöltés Archívum letöltése Letöltési hivatkozás részletei Engedélyezés a közvetlen csevegésekben (BÉTA)! @@ -1669,24 +1669,24 @@ Kép a képben hívások Biztonságosabb csoportok Használja az alkalmazást hívás közben. - Vagy illessze be az archívum hivatkozását - Az archívum hivatkozásának beillesztése + Vagy az archívum hivatkozásának beillesztése + Archívum hivatkozásának beillesztése Letöltés ismét Sikertelen importálás Ellenőrizze, hogy a hálózati beállítások megfelelőek-e ehhez az eszközhöz. - A folytatáshoz a csevegést le kell állítani. - Csevegés leállítása - Vagy ossza meg biztonságosan ezt a fájl hivatkozást + A folytatáshoz a csevegést meg kell szakítani. + Csevegés megállítása folyamatban + Vagy a fájl hivítkozásának biztonságos megosztása Csevegés indítása Nem szabad ugyanazt az adatbázist használni egyszerre két eszközön.]]> Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez. Átköltöztetés egy másik eszközről opciót az új eszközön és szkennelje be a QR-kódot.]]> - Az átköltöztetés véglegesítése - Az átköltöztetés véglegesítése egy másik eszközön. + Átköltöztetés véglegesítése + Átköltöztetés véglegesítése egy másik eszközön. Letöltés előkészítése Feltöltés ismét %s feltöltve - A feltöltés sikertelen + Sikertelen feltöltés Archívum feltöltése Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, továbbá üzenetkézbesítési hibákat okozhat Importálás ismét @@ -1706,9 +1706,62 @@ Hibás hivatkozás végpontok közötti kvantumrezisztens titkosítás Ez a csevegés végpontok közötti titkosítással védett. - A költöztetési párbeszédablak megnyitása + Átköltöztetési párbeszédablak megnyitása Ez a csevegés végpontok közötti kvantumrezisztens tikosítással védett. végpontok közötti titkosítással és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi.]]> végpontok közötti kvantumrezisztens titkosítással és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi.]]> Hiba az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. + Keresse meg ezt az engedélyt az Android beállításaiban, és adja meg kézzel. + Engedélyezés a beállításokban + Engedély(ek) megadása a hívások kezdeményezéséhez + Mikrofon + Beállítások megnyitása + Kamera + Kamera és mikrofon + Engedélyek megadása + Vezetékes Ethernet + Mobilhálózat + Internetkapcsolat + Nincs hálózati kapcsolat + További + Wi-Fi + továbbított + A SimpleX hivatkozások küldése le van tiltva + A csoport tagjai küldhetnek SimpleX hivatkozásokat. + tulajdonosok + adminok + minden tag + SimpleX hivatkozás + A hangüzenetek küldése le van tiltva + A SimpleX hivatkozások küldése ebben a csoportban le van tiltva. + A SimpleX hivatkozások küldése le van tiltva + Fájlok és média tartalom küldése le van tiltva + A SimpleX hivatkozások küldése engedélyezve van. + Számukra engedélyezve: + mentett + mentve innen: %s + Továbbítva innen: + A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet. + Mentett + Mentve innen: + Letöltés + Továbbítás + Továbbított + Üzenet továbbítása… + Fejhallgató + Hangszóró + Fülhallgató + Egyidejű fogadás + Bluetooth + A közvetlen csevegésekben engedélyezve lesz! + Litván kezelőfelület + Megbízhatóbb hálózati kapcsolat. + Hálózatkezelés + Amikor egy bejövő hang- vagy videóhívás érkezik. + Üzenetek továbbítása és mentése + Bejövő hívás csengőhangja + Az üzenet forrása titokban marad. + Profilképek + Profilkép alakzat + Négyzet, kör vagy bármi a kettő között. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg new file mode 100644 index 0000000000..9680161c4f --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg new file mode 100644 index 0000000000..53bc5becaa --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg new file mode 100644 index 0000000000..fef2b1d2ce --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_brand_awareness_filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg new file mode 100644 index 0000000000..7ad0b14b70 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg new file mode 100644 index 0000000000..2bda1e9d74 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg new file mode 100644 index 0000000000..766dc76be8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg new file mode 100644 index 0000000000..513bb38c40 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_dark@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_dark@4x.png new file mode 100644 index 0000000000..45fc44af43 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_dark@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_light@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_light@4x.png new file mode 100644 index 0000000000..c372e25b76 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex_light@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg new file mode 100644 index 0000000000..068bfc1a82 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 69710a68a2..310a021ff2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1715,4 +1715,58 @@ Non devi usare lo stesso database su due dispositivi.]]> Puoi fare un altro tentativo. Puoi fare un altro tentativo. + Errore nel mostrare la notifica, contatta gli sviluppatori. + Trova questa autorizzazione nelle impostazioni di Android e concedila a mano. + Fotocamera + Fotocamera e microfono + Concedi autorizzazioni + Concedi nelle impostazioni + Concedi la/le autorizzazione/i per effettuare chiamate + Microfono + Apri le impostazioni + inoltrato + Connessione di rete + Nessuna connessione di rete + Mobile + Altro + WiFi + Cavo ethernet + Link di SimpleX non consentiti + File e multimediali non consentiti + Messaggi vocali non consentiti + Link di SimpleX + Consenti di inviare link di SimpleX. + Vieta l\'invio di link di SimpleX + Attivo per + I membri del gruppo possono inviare link di Simplex. + proprietari + amministratori + tutti i membri + I link di SimpleX sono vietati in questo gruppo. + salvato + Salvato da + Inoltra messaggio… + Inoltrato + Inoltrato da + Scarica + Inoltra + I destinatari non possono vedere da chi proviene questo messaggio. + Salvato + salvato da %s + Bluetooth + Auricolari + Cuffie + Altoparlante + Ricezione concomitanza + Viene attivata nelle chat dirette! + Inoltra e salva i messaggi + Quando si connettono le chiamate audio e video. + Interfaccia in lituano + Connessione di rete più affidabile. + Gestione della rete + Immagini del profilo + Suoni nelle chiamate + La fonte del messaggio resta privata. + Forma delle immagini del profilo + Quadrata, circolare o qualsiasi forma tra le due \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 7bc282de3a..83d59b3705 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -46,7 +46,7 @@ 太文字 音声通話 音声とビデオ通話 - ※注意※:喪失したら、パスフレーズの回復・変更ができません。]]> + ※注意※:紛失するとパスフレーズの回復・変更ができません。]]> グループ全員の接続が継続します。 送信相手が消えるメッセージを送るのを許可する。 送信相手が永久メッセージ削除するのを許可する。(24時間) @@ -116,7 +116,7 @@ アプリを更新し、開発者にご連絡ください。 即時通知! 即時通知が無効になってます! - 暗証フレーズが必要 + パスフレーズが必要 プライベート 連絡先の名前 連絡先が非表示: @@ -179,7 +179,7 @@ チャットが停止してます。 承諾すると、メッセージのサーバがIPアドレス、ISP、接続サーバを特定できます。 チャット設定 - 消す + チャットを削除 閉じるボタン 未検証の状態に戻す 画像をギャラリーに保存しました。 @@ -192,8 +192,8 @@ プライバシーとセキュリティ強化 シークレットモード %s の新機能 - 新しい暗証フレーズ - 一度も + 新しいパスフレーズ + なし 新しいメッセージ いいえ 通知 @@ -235,8 +235,8 @@ 音声メッセージを送れるのはあなたの連絡相手だけです。 データベース暗号化のパスフレーズが更新され、キーストア (暗証キー保管庫)に保存されます。 データベースエラー - データベースはランダムな暗証フレーズで暗号化済みです。エキスポートする前に更新してください。 - データベース暗証フレーズはキーストア (暗証キー保管庫) に保存された暗証フレーズと異なります。 + データベースはランダムなパスフレーズで暗号化済みです。エクスポートする前に更新してください。 + データベースパスフレーズはキーストア (暗証キー保管庫) に保存されたパスフレーズと異なります。 %d日 %d 日 削除 @@ -268,7 +268,7 @@ QRコード リンクが正しいかどうかご確認ください。または、連絡相手にもう一度リンクをお求めください。 %1$s との接続を確認し、もう一度お試しください。 - 現在の正しい暗証フレーズを入力してください。 + 現在の正しいパスフレーズを入力してください。 プレセットサーバ プレセットサーバのアドレス 接続待ち… @@ -352,8 +352,8 @@ 通話 チャットは稼働中 チャットが停止してます。 - データベース暗証フレーズ - データベースをエキスポート + データベースパスフレーズ + データベースをエクスポート データベースを削除 データベースを読み込みますか? 新しいデータベースのアーカイブ @@ -363,14 +363,14 @@ %d ファイル|合計: %s 暗号化する 削除 - 現在の暗証フレーズ + 現在のパスフレーズ データベースはランダムなパスフレーズで暗号化済みで、変更可能です。 - データベースの暗証フレーズを更新しますか? + データベースのパスフレーズを更新しますか? データベースが暗号化されます。 データベースが暗号化され、パスフレーズがキーストア (暗証キー保管庫) にほぞんされます。 キーストア (暗証キーの保管庫) にアクセスできないため、データベースのパスワードが保存できません。 データベース暗号化のパスフレーズが更新されます。 - チャットを開くにはデータベース暗証フレーズが必要です。 + チャットを開くにはデータベースパスフレーズが必要です。 ファイル: %s 作成日時 %1$s アーカイブを削除 @@ -448,7 +448,7 @@ コアのバージョン: v%s 画像を編集 不在着信 - リレー経由で繋がる。 + 常にリレーを使用 エンドツーエンド暗号化済み チャットのデータベースが削除されました。 次の期間が経ったら、メッセージを削除: @@ -458,10 +458,10 @@ データベース暗号化でエラー発生 新しいパスフレーズを確認… データベースを暗号化しますか? - 暗証フレーズを失くさないように保管してください。失くすとチャットにアクセスできなくなります。 - 正しい暗証フレーズを入力 - 暗証フレーズを入力 - キーストア (暗証キー保管庫) に暗証フレーズが見つかりません。入力してください。アプリのデータをバックアップのツールで復元させると暗証フレーズが見つかりかねます。そうではない場合は開発者にご連絡ください。 + パスフレーズを失くさないように保管してください。失くすとチャットにアクセスできなくなります。 + 正しいパスフレーズを入力 + パスフレーズを入力 + キーストア (パスキー保管庫) にパスフレーズが見つかりません。手動で入力してください。これは、バックアップツールを使用してアプリのデータを復元した場合に発生する可能性があります。そうではない場合は開発者にご連絡ください。 リンクを発行する グループ グループのプロフィール保存でエラー発生 @@ -523,7 +523,7 @@ 接続待ちの繋がりを削除しますか? 接続 使い捨ての招待リンク - データベース暗証フレーズとエキスポート + データベースパスフレーズとエクスポート 使い方 マークダウン (書式編集) ガイド サーバを手動で入力 @@ -585,7 +585,7 @@ 間接 (%1$s) シークレットモード シークレット モードでは、連絡先ごとに新しいランダムなプロファイルを使用してプライバシーを保護します。 - いいえ + オフ オン ダイレクトメッセージ 消えるメッセージ @@ -612,9 +612,9 @@ アプリが起動中のみに通知が出ます! 暗号化データベース キーチェーンのエラー - 暗証フレーズを失くさないように保管してください。失くすと変更できなくなります。 + パスフレーズを失くさないように保管してください。失くすと変更できなくなります。 エラー: %s - データベースのバックアップで復旧する際に前の暗証フレーズを入力してください。行われたら元に戻せません。 + データベースのバックアップで復旧する際に前のパスフレーズを入力してください。行われたら元に戻せません。 メンバーを招待する 脱退しました。 %s の役割を %s に変えました。 @@ -685,7 +685,7 @@ 送信元が繋がりリクエストを削除したかもしれません。 このサーバで待ち行列を作るには認証が必要です。パスワードをご確認ください。 SimpleXロック - 通知を受けるには、データベースの暗証フレーズを入力してください。 + 通知を受けるには、データベースのパスフレーズを入力してください。 SimpleX Chat サービス SimpleXロック:オン 起動時、または非アクティブ状態で30秒が経った後に戻ると、認証する必要となります。 @@ -739,9 +739,9 @@ .onionホストを使う 利用可能時に トランスポート隔離 - 保存して、連絡先に公開 + 保存して連絡先に公開 現在のプロフィール - 保存して、連絡先に公開 + 保存して連絡先に公開 あなたのプライバシーとセキュリティを守るメッセージとアプリのプラットフォーム 連絡先情報と届けたメッセージをサーバに保存することは一切ありません。 あなたのチャットはあなたが決めます! @@ -768,7 +768,7 @@ ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。 選択中の以前の送受信したメッセージが削除されます (※元に戻せません※)。数分かかります。 更新 - チャットデータベースが暗号化されてません。暗証フレーズを設定して保管してください。 + チャットデータベースが暗号化されてません。パスフレーズを設定して保管してください。 データベースを復元 アプリの設定・データベース経由で、またはアプリの再起動でチャットを始められます。 グループ招待が届きました。参加してグループのメンバーとお繋がりください。 @@ -827,12 +827,12 @@ あなたの通話 %1$dスキップしたメッセージ リンクのプレビューを送信 - 暗証フレーズを設定してからエキスポート + パスフレーズを設定してからエクスポート チャットを閉じますか? - 暗証フレーズをキーストア (暗証キー保管庫) に保存 + パスフレーズをキーストア (パスキー保管庫) に保存 不正なパスフレーズ 不明なエラー - 暗証フレーズをを保存して、チャットを開始 + パスフレーズを保存して、チャットを開始 不明なデータベースのエラー: %s 不正なパスフレーズです! グループ招待が届きました @@ -868,7 +868,7 @@ サーバのQRコードを読み込む テストに失敗したサーバがあります: SimpleX Chatサーバを使いますか? - SimpleX Chatを使っています。 + SimpleX Chatサーバを使用中。 リンクを送る simplexmq: バージョン%s (%2s) 応答を待機中… @@ -881,8 +881,8 @@ スピーカーオン あなたのプライバシー この設定は現在のチャットプロフィールのメッセージに適応されます。 - アプリ起動時に暗証フレーズを入力しなければなりません。端末に保存されてません。 - データベースの暗証フレーズ変更が完了してません。 + アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。 + データベースのパスフレーズ変更が完了してません。 リンク、またはQRコードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。 元に戻す 更新 @@ -893,7 +893,7 @@ トランスポート隔離 復元 データベースを復元しますか? - 保存して、グループのメンバーにに知らせる + 保存してグループのメンバーに知らせる メールを送る メディア共有… SimpleXリンク @@ -924,9 +924,9 @@ あなたのランダム・プロフィール 音声メッセージ SOCKSプロキシ - データベースのエキスポート、読み込み、削除するにはチャット機能を停止する必要があります。チャット機能を停止すると送受信ができなくなります。 + データベースのエクスポート、読み込み、削除するにはチャット機能を停止する必要があります。チャット機能を停止すると送受信ができなくなります。 あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。 - データベース暗証フレーズを更新 + データベースパスフレーズを更新 アーカイブを保存 あなたが自分の役割を次に変えました:%s アドレスを変えました @@ -1156,8 +1156,8 @@ アドレス設定エラー アドレスをリンクまたは QR コードとして共有すると、誰でもあなたに接続できます。 後でアドレスを削除しても、連絡先が失われることはありません。 - 連絡先との接続がそのまま続きます。 - 連絡先との接続がそのまま続きます。 プロフィールの更新が連絡先に共有されます。 + 連絡先との接続は維持されます。 + 連絡先との接続は維持されます。 プロフィールの更新が連絡先に共有されます。 SimpleX のアドレスを作成 連絡先に公開する プロフィールの更新は連絡先に送信されます。 @@ -1378,7 +1378,7 @@ パスフレーズを変更するかアプリを再起動すると、平文として設定に保存されます。 パスフレーズは平文として設定に保存されます。 注意: メッセージとファイルのリレーは SOCKS プロキシ経由で接続されます。 通話とリンク プレビューの送信には直接接続が使用されます。]]> - ローカルファイルを暗号化する + ローカルファイルを暗号化 開く 保存されたファイルとメディアを暗号化する メンバー連絡先の作成中にエラーが発生 @@ -1509,7 +1509,7 @@ メンバーのブロックを解除? グループ作成:新しいグループを作成する。]]> モバイルアプリからデスクトップへの接続を許可するには、ファイアウォールでこのポートを開いてください。 - ブロックしたメンバー + メンバーのブロック 表示可能な履歴 デスクトップから使用 セッションコード @@ -1603,7 +1603,7 @@ 通話終了 ブラウザ起動エラー 別の端末から移行 - 暗証フレーズを設定 + パスフレーズを設定 %s をブロック済 管理者によりブロック済 暗号化されたファイルとメディア @@ -1716,4 +1716,57 @@ メッセージ保存済 チャット停止中 エラーが表示された場合は、開発者に連絡してください。 + Androidの設定から権限を設定見つけ、手動で許可。 + 設定で付与 + 権限の付与 + カメラ + カメラとマイク + マイク + 通話許可を付与 + 設定を開く + モバイル回線 + ネットワーク接続なし + Wi-Fi + 有線イーサネット + ネットワーク接続 + その他 + グループメンバーはSimpleXリンクを送信できます。 + オーナー + このグループではSimpleXリンクは禁止されています。 + SimpleXtリンクは許可されていません + 管理者 + 全てのメンバー + SimpleXリンクの送信を許可 + 音声メッセージは許可されていません + ファイルとメディアは許可されていません + SimpleXリンクの送信禁止 + SimpleXリンク + ブルートゥース + 有効化 + イヤホン + ヘッドホン + スピーカー + 着信音 + 音声通話とビデオ通話の接続時 + ダイレクトチャットが有効化されます + より信頼性の高いネットワーク接続 + ネットワーク管理 + 保存済 + %sから保存 + 転送済 + 転送元 + 保存元 + 転送 + 転送済 + ダウンロード + メッセージの転送と保存 + メッセージの転送 + リトアニア語UI + メッセージ発信者は非公開のままです + 並列受信 + 受信者は発信者を特定できません。 + 保存済 + プロフィール画像 + 正方形、円形またはその中間 + プロフィール画像をシェイプ \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index 57ce7eb06e..43237fa2a9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -7,7 +7,7 @@ a + b Apie SimpleX Pridėti serverį… - Pridėti serverius skenuojant QR kodus. + Pridėti serverius nuskanuojant QR kodus. Išvaizda Programėlės versija Programėlės versija: v%s @@ -18,13 +18,13 @@ Atsiliepti Skambutis baigtas SKAMBUČIAI - Leisti jūsų adresatams negrįžtamai ištrinti išsiųstas žinutes. + Leisti jūsų kontaktams negrįžtamai ištrinti išsiųstas žinutes. (24 valandas) Atgal PROGRAMĖLĖS PIKTOGRAMA visada - Leisti jūsų adresatams siųsti balso žinutes. - Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų adresatas jums tai leidžia. - Leisti balso žinutes tik tuo atveju, jei jūsų adresatas jas leidžia. + Leisti jūsų kontaktams siųsti balso žinutes. + Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų kontaktas jums tai leidžia. (24 valandas) + Leisti balso žinutes tik tuo atveju, jei jūsų kontaktas jas leidžia. Leisti Leisti balso žinutes\? pusjuodis @@ -33,14 +33,14 @@ Garso ir vaizdo skambučiai bloga žinutės maiša blogas žinutės ID - Leisti išnykstančias žinutes tik tuo atveju, jei jūsų adresatas jas leidžia. - Visos žinutės bus ištrintos – to neįmanoma bus atšaukti! Žinutės bus ištrintos TIK jums. - Leisti negrįžtamai ištrinti išsiųstas žinutes. + Leisti išnykstančias žinutes tik tuo atveju, jei jūsų kontaktas jas leidžia. + Visos žinutės bus ištrintos – to bus neįmanoma atšaukti! Žinutės bus ištrintos TIK jums. + Leisti negrįžtamai ištrinti išsiųstas žinutes. (24 valandas) Leisti siųsti išnykstančias žinutes. Leisti siųsti balso žinutes. Leisti siųsti tiesiogines žinutes nariams. Garso ir vaizdo skambučiai - Leisti jūsų adresatams siųsti išnykstančias žinutes. + Leisti jūsų kontaktams siųsti išnykstančias žinutes. Tapatybės nustatymas neprieinamas Turėkite omenyje: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.]]> Atsisakyti @@ -49,14 +49,14 @@ prisijungta jungiamasi ryšys užmegztas - jungiasi… + jungiamasi… Ryšio klaida Prisijungti Prisijungė Prisijungti prisijungė - jungiasi… - jungiasi… + jungiamasi… + jungiamasi… Prisijungta Patvirtinti Konfigūruoti ICE serverius @@ -64,7 +64,7 @@ prisijungė Ryšys Patvirtinti duomenų bazių naujinimus - jungiasi + jungiamasi Ryšys Duomenų bazės šifravimo slaptafrazė bus atnaujinta. Branduolio versija: v%s @@ -97,11 +97,11 @@ Klaida prisijungiant prie grupės Klaida gaunant failą Klaida keičiant adresą - Klaida ištrinant adresatą + Klaida ištrinant kontaktą Klaida ištrinant grupę Atsijungti Klaida ištrinant naudotojo profilį - Slėpti adresatą ir žinutę + Slėpti kontaktą ir žinutę Kopijuoti Atsakyti Ištrinti žinutę\? @@ -111,7 +111,7 @@ Failas įrašytas Atsijungta Klaida - Paprašykite adresato, kad įjungtų balso žinučių siuntimą. + Paprašykite kontakto, kad įjungtų balso žinučių siuntimą. Atstatyti Ištrinti QR kodas @@ -146,8 +146,8 @@ Visiems taisyta Susisiekite su grupės administratoriumi. - Ištrinti adresatą - Ištrinti adresatą\? + Ištrinti kontaktą + Ištrinti kontaktą? Sukurti slaptą grupę Iš galerijos Leidimas atmestas! @@ -190,14 +190,14 @@ Tamsus Atstatyti spalvas Įrašyti spalvą - Grupės nuostatos + Grupės parinktys Ištrinti visiems Tiesioginės žinutės Išnykstančios žinutės Išnykstančios žinutės Sąsaja prancūzų kalba Patobulintas privatumas ir saugumas - Pažymėta adresatų: %d + Pažymėta kontaktų: %d Ištrinti pokalbio profilį\? Klaida importuojant pokalbio duomenų bazę Ištrinti žinutes po @@ -234,13 +234,13 @@ Ištrinti profilį Sukurti vienkartinio pakvietimo nuorodą Ištrinti - Įrašyti ir pranešti adresatui + Įrašyti ir pranešti kontaktui Įjungti automatinį žinučių ištrynimą\? Klaida ištrinant pokalbio duomenų bazę Klaida pradedant pokalbį Slėpti profilį Importuoti - Įrašyti ir pranešti adresatams + Įrašyti ir pranešti kontaktams pradedama… Naudoti pokalbį Išjungti garsiakalbį @@ -249,7 +249,7 @@ NUSTATYMAI Sistemos nežinomas žinutės formatas - SimpleX adresato adresas + SimpleX kontakto adresas SimpleX grupės nuoroda SimpleX vienkartinis pakvietimas SimpleX nuorodos @@ -268,11 +268,11 @@ Atnaujinti tinklo nustatymus\? SimpleX Chat žinutės SimpleX Chat skambučiai - Rodyti adresatą ir žinutę - Rodyti tik adresatą - Stabdyti pokalbį + Rodyti kontaktą ir žinutę + Rodyti tik kontaktą + Sustabdyti pokalbius Bendrinti žinutę… - Kamera + Fotoaparatas Dėkojame, kad įdiegėte SimpleX Chat! Naudoti serverį Naudoti SOCKS įgaliotąjį serverį\? @@ -283,12 +283,12 @@ Bluetooth palaikymas ir kiti patobulinimai. Bakstelėkite, norėdami prisijungti Rodyti - Stabdyti + Sustabdyti Naujinti ir atverti pokalbį Perjungti neskaityta Rodyti: - Stabdyti pokalbį\? + Sustabdyti pokalbius? Atnaujinti Nežinoma klaida Pradėti naują pokalbį @@ -302,7 +302,7 @@ SimpleX jūs per grupės nuorodą - per adresato adreso nuorodą + per kontakto adreso nuorodą per vienkartinę nuorodą per %1$s Per naršyklę @@ -326,12 +326,12 @@ Naudojami SimpleX Chat serveriai. Išplėstiniai tinklo nustatymai Jūsų dabartinis profilis - Jūsų profilis yra saugomas jūsų įrenginyje ir bendrinamas tik su jūsų adresatais. SimpleX serveriai negali matyti jūsų profilio. + Jūsų profilis yra saugomas jūsų įrenginyje ir bendrinamas tik su jūsų kontaktais. SimpleX serveriai negali matyti jūsų profilio. vaizdo skambutis Jūsų skambučiai WebRTC ICE serveriai Jūsų ICE serveriai - jūs išėjote + išėjote administratorius Jūsų atsitiktinis profilis Jūs leidžiate @@ -363,17 +363,17 @@ Išnykstančios žinutės šiame pokalbyje yra uždraustos. Balso žinutės šioje grupėje yra uždraustos. Prisijungti prie grupės? - Prisijungti per adresato adresą? + Prisijungti per kontakto adresą? failų siuntimas kol kas nepalaikomas neteisingas žinutės formatas failų gavimas kol kas nepalaikomas neteisingas pokalbis neteisingi duomenys inkognito per vienkartinę nuorodą - Adresatas jau yra + Kontaktas jau yra Nepavyksta inicijuoti duomenų bazės Žinutės tekstas - Paslėptas adresatas: + Paslėptas kontaktas: Bendrinti mediją… Didelis failas! Balso žinutės uždraustos! @@ -400,15 +400,15 @@ Šalinti narį Šviesus įjungta jums - Tiek jūs, tiek ir jūsų adresatas gali siųsti išnykstančias žinutes. + Tiek jūs, tiek ir jūsų kontaktas gali siųsti išnykstančias žinutes. Grupės nariai gali siųsti balso žinutes. SimpleX Chat saugumo auditą atliko „Trail of Bits“. Saugumo įvertinimas Įrašyti grupės profilį - Tiek jūs, tiek ir jūsų adresatas gali negrįžtamai ištrinti išsiųstas žinutes. - Tiek jūs, tiek ir jūsų adresatas gali siųsti balso žinutes. + Tiek jūs, tiek ir jūsų kontaktas gali negrįžtamai ištrinti išsiųstas žinutes. (24 valandas) + Tiek jūs, tiek ir jūsų kontaktas gali siųsti balso žinutes. Norėdami gauti pranešimus, įveskite duomenų bazės slaptafrazę - Adresato vardas + Kontakto vardas SimpleX užraktas Įjungti SimpleX užraktas įjungtas @@ -425,7 +425,7 @@ Patikrinkite serverio adresą ir bandykite dar kartą. Neteisingas serverio adresas! Pokalbio profilis - Profilis yra bendrinamas tik su jūsų adresatais. + Profilis yra bendrinamas tik su jūsų kontaktais. „GitHub“ saugykloje.]]> SOCKS ĮGALIOTASIS SERVERIS Įrašyti slaptafrazę ir atverti pokalbį @@ -440,14 +440,14 @@ Išvalyti Išeiti iš grupės Vaidmuo - Grupė yra pilnai decentralizuota – ji yra matoma tik nariams. + Pilnai decentralizuota – ji yra matoma tik nariams. Apipavidalinimas - įjungta adresatui + įjungta kontaktui Išnykstančios žinutės šioje grupėje yra uždraustos. Grupės nariai gali siųsti tiesiogines žinutes. Grupės nariai gali siųsti išnykstančias žinutes. Slėpti programėlės ekraną paskiausių programėlių sąraše. - Adresatas leidžia + Kontaktas leidžia Balso žinutės šiame pokalbyje yra uždraustos. Išsiųstos žinutės bus ištrintos po nustatyto laiko. Nebeslėpti @@ -463,7 +463,7 @@ Vaidmuo bus pakeistas į „%s“. Visiems grupėje bus pranešta. Vaidmuo bus pakeistas į „%s“. Narys gaus naują pakvietimą. Pokalbio nuostatos - Adresato nuostatos + Kontakto nuostatos Prisijungti Keisti SERVERIAI @@ -497,13 +497,13 @@ Žymėti kaip skaitytą Prievadas Failo siuntimas bus sustabdytas. - Stabdyti + Sustabdyti Stabdyti failą Stabdyti failo gavimą\? Stabdyti failo siuntimą\? Šis tekstas yra prieinamas nustatymuose Garso/vaizdo skambučiai yra uždrausti. - Tiek jūs, tiek ir jūsų adresatas gali skambinti. + Tiek jūs, tiek ir jūsų kontaktas gali skambinti. Žinutės juodraštis Apsaugokite slaptažodžiu savo pokalbių profilius! Blogas žinutės ID @@ -531,21 +531,21 @@ 5 minutės Mažiausios akumuliatoriaus sąnaudos. Jūs gausite pranešimus tik tada, kai programėlė bus atverta (BE foninės tarnybos).]]> Nutraukti adreso keitimą - Automatiškai priimti adresatų užklausas + Automatiškai priimti kontaktų užklausas Jį įrašius visi duomenys bus pašalinti. - Kiekvienam adresatui ir grupės nariui bus naudojamas atskiras TCP ryšys (ir SOCKS prisijungimo duomenys). -\nTurėkite omenyje: jei turite daug ryšių, akumuliatoriaus ir interneto duomenų sąnaudos gali būti žymiai didesnės ir, kartais, ryšiai gali patirti nesėkmę. + Kiekvienam kontaktui ir grupės nariui bus naudojamas atskiras TCP prisijungimas (ir SOCKS prisijungimo duomenys). +\nTurėkite omenyje: jei turite daug prisijungimų, akumuliatoriaus ir interneto duomenų sąnaudos gali būti žymiai didesnės ir, kartais, prisijungimai gali patirti nesėkmę. %1$s nori su jumis susisiekti per Yra įjungtas akumuliatoriaus naudojimo optimizavimas, išjungiantis foninę tarnybą ir periodines užklausas apie naujas žinutes. Nustatymuose galite įjungti ją iš naujo. Visada įjungta Programėlė galės gauti sistemos pranešimus tik tada, kai veiks. Jokia foninė tarnyba nebus paleidžiama Priimti - Priimti inkognito adresatą + Priimti inkognito režimu Pridėti iš anksto parinktus serverius Administratoriai gali kurti prisijungimo prie grupių nuorodas. Foninė tarnyba visada veikia – pranešimai bus rodomi iš karto, kai tik bus prieinamos žinutės. Patvirtinti tapatybę - Pridėkite adresą prie savo profilio, kad jūsų adresatai galėtų bendrinti jį su kitais žmonėmis. Apie profilio atnaujinimą bus pranešta jūsų adresatams. + Pridėkite adresą prie savo profilio, kad jūsų kontaktai galėtų bendrinti jį su kitais žmonėmis. Apie profilio atnaujinimą bus pranešta jūsų kontaktams. Nutraukti adreso keitimą? Adreso keitimas bus nutrauktas. Bus naudojamas senas priėmimo adresas. 1 minutė @@ -560,28 +560,28 @@ Leisti siųsti failus ir mediją. Nutraukti Praleista %1$d žinučių. - Jūsų adresatai nepraras ryšio su jumis. + Jūsų kontaktai išliks prisijungę prie jūsų. atsilieptas skambutis Priimti Praleista žinučių: %1$d. Programėlės duomenų atsarginė kopija Akcentavimas - Paleidus programėlę iš naujo ar pakeitus slaptafrazę, saugiam slaptafrazės saugojimui bus naudojama „Android Keystore“ – tai leis gauti pranešimus. + Paleidus programėlę iš naujo ar pakeitus slaptafrazę, saugiam slaptafrazės saugojimui bus naudojama Android raktų saugykla – tai leis gauti pranešimus. Fonas 30 sekundžių - Leisti reaguoti į žinutes tik tuo atveju, jeigu tai leidžia jūsų adresatas. + Leisti reaguoti į žinutes tik tuo atveju, jeigu tai leidžia jūsų kontaktas. Apie SimpleX adresą Ar gauti prieigą prie serverių per SOCKS įgaliotąjį serverį naudojant prievadą %d? Prieš įjungiant šią parinktį, privalo būti paleistas įgaliotasis serveris. - Jūsų adresatai nepraras ryšio su jumis. Apie profilio atnaujinimą bus pranešta jūsų adresatams. - garso skambutis (nėra užšifruotas nuo vieno galo iki kito galo) + Jūsų kontaktai išliks prisijungę prie jūsų. Apie profilio atnaujinimą bus pranešta jūsų kontaktams. + garso skambutis (nėra užšifruotas visapusiškai) NARIŲ: %1$s Adresas Priimti - Leisti skambučius tik tuo atveju, jeigu juos leidžia jūsų adresatas. + Leisti skambučius tik tuo atveju, jeigu juos leidžia jūsų kontaktas. sutariama dėl šifravimo… sutariama dėl %s šifravimo… - Leisti jūsų adresatams reaguoti į žinutes. - Leisti jūsų adresatams jums skambinti. + Leisti jūsų kontaktams reaguoti į žinutes. + Leisti jūsų kontaktams jums skambinti. Leisti reaguoti į žinutes. Geresnės žinutės Tapatybės patvirtinimas patyrė nesėkmę @@ -606,7 +606,7 @@ Atšaukti failo peržiūrą Atšaukti vaizdo peržiūrą Duomenų bazės ID: %d - Ištrinti ir pranešti adresatui + Ištrinti ir pranešti kontaktui Tinkintas laikas Arabų, bulgarų, suomių, hebrajų, tajų ir ukrainiečių – dėkojame naudotojams ir Weblate. 6 naujos naudotojo sąsajos kalbos @@ -622,12 +622,12 @@ Pokalbio duomenų bazė ištrinta Pasirinkti failą Talkinti - Adresatai + Kontaktai Plėtotojo įrankiai - Visos žinutės bus ištrintos – to neįmanoma bus atšaukti! - Pridėti adresatą: sukurti naują pakvietimo nuorodą arba prisijungti per gautą nuorodą.]]> - Kamera neprieinama - Adresatai gali žymėti žinutes ištrynimui; jūs galėsite jas peržiūrėti. + Visos žinutės bus ištrintos – to bus neįmanoma atšaukti! + Pridėti kontaktą: sukurti naują pakvietimo nuorodą arba prisijungti per gautą nuorodą.]]> + Fotoaparatas neprieinamas + Kontaktai gali pažymėti žinutes ištrynimui; jūs galėsite jas peržiūrėti. Sukurti profilį Kuriama nuoroda… ištrinta @@ -642,7 +642,7 @@ %dval. Kritinė klaida Sukurti grupę: sukurti naują grupę.]]> - Pridėti adresatą + Pridėti kontaktą Tinkinti apipavidalinimą POKALBIO DUOMENŲ BAZĖ Naudotojo sąsaja kinų ir ispanų kalbomis @@ -687,7 +687,7 @@ - stabilesnis žinučių pristatymas. \n- šiek tiek geresnės grupės. \n- ir daugiau! - Tik jūsų adresatas gali skambinti. + Tik jūsų kontaktas gali skambinti. Atnaujinkite programėlę ir susisiekite su plėtotojais. Atverti niekada @@ -721,7 +721,7 @@ ne Uždrausti garso/vaizdo skambučius. Tik jūs galite siųsti išnykstančias žinutes. - Tik jūsų adresatas gali siųsti išnykstančias žinutes. + Tik jūsų kontaktas gali siųsti išnykstančias žinutes. (šis įrenginys v%s)]]> Vienu metu gali veikti tik vienas įrenginys Nesuderinama! @@ -731,8 +731,1037 @@ Bendraukime per Simplex Chat Žinutė per didelė Štai kas naujo %s versijoje - Tik jūsų adresatas gali siųsti balso žinutes. + Tik jūsų kontaktas gali siųsti balso žinutes. Tik grupės savininkai gali įjungti failus ir mediją. Tik grupės savininkai gali keisti grupės nuostatas. Apipavidalinimo importavimo klaida + Prašome pasitikrinti savo tinklo ryšį su %1$s ir bandykite vėl. + Tikrina naujas žinutes kas 10 minučių iki 1 minutės + Nėra programėlės prieigos kodo + Grupės pakvietimas nebegalioja + pakviestas per jūsų grupės nuorodą + Administratoriai gali užblokuoti narį visiems. + Visos naujos žinutės nuo %s bus paslėptos! + Fotoaparatas ir mikrofonas + pakeitė %s rolę į %s + pakeitė tavo rolę į %s + Prisijungti prie savo grupės? + Prijungtas darbastalis + prisijungė tiesiogiai + Prisijungta prie darbastalio + Jungiamasi prie darbastalio + Prisijungimo užklausa išsiųsta! + prisijungimas %1$d + Privatūs užrašai + Ryšiui skirtas laikas + Netinkama prisijungimo nuoroda + Ištrinti eilę + Palyginti failą + Periodiniai pranešimai + Periodiniai pranešimai yra išjungti! + Paslėptas + Įrenginio autentifikacija neįjungta. Galite įjungti SimpleX užraktą nustatymuose, įjungus įrenginio autentifikaciją. + Atidaryti perkėlimo ekraną + Ištrinti nario žinutę? + Kalbėti su kūrėjais + Paprašyta gauti nuotrauką + Prašome palaukti kol failas yra kraunamas iš susieto mobiliojo + Mėgstamas + Ištrinti laukiantį prisijungimą? + Sukurti pokalbių profilį + Jei patvirtinsite, žinučių serveriai galės matyti jūsų IP adresą, ir jūsų tiekėjas - prie kurių serverių jungiatės. + Atkreipkite dėmesį: žinučių ir failų perdavimas yra prijungtas per SOCKS tarpinį serverį. Skambučiams ir nuorodų peržiūrų siuntimui naudojamas tiesioginis ryšys.]]> + Netinkamas vardas! + Sukurti savo profilį + Asmenys gali prisijungti prie jūsų tik per nuorodas kuriomis dalinatės. + Tai gali būti pakeista vėliau nustatymuose. + Tai gali įvykti kai: +\n1. Žinutės galiojimo laikas baigėsi siuntėjo programoje po 2 dienų arba serveryje po 30 dienų. +\n2. Žinutės atšifravimas nepavyko, nes jūs ar jūsų kontaktas naudojo seną duomenų bazės atsarginę kopiją. +\n3. Prisijungimas buvo kompromisuotas. + Prieigos kodas pakeistas! + Patvirtinti naują slaptafrazę… + Prašome įvesti tinkamą dabartinę slaptafrazę. + Raktų grandinės klaida + kontaktas %1$s pasikeitė į %2$s + Kvantiškai atsparus visapusis šifravimas + Grupė bus ištrinta visiems nariams - tai negali būti atšaukta! + Grupė bus ištrinta jums - tai negali būti atšaukta! + Sukurta + (dabartinis) + Ištrinta: %s + Grupės narys nepalaiko sutaisymo + PING skaičius + Tai leidžia daug anonimiškų prisijungimų nesidalinant jokiais bendrais duomenimis tarp jų, tame pačiame pokalbių profilyje. + Uždrausti siųsti balso žinutes. + Grupės nariai gali reaguoti į žinutes. + %d diena + Palyginkite saugumo kodus su savo kontaktais. + Nustatykite jį vietoje sistemos autentifikacijos. + Tinkinkite ir bendrinkite spalvų temas. + Pasilikite savo prisijungimus + Sutaisykite šifravimą po atsarginių kopijų atstatymo. + Inkognito grupės + Įklijuokite nuorodą, kad prisijungti! + Vengrų ir turkų UI + pasirinktinis + Darbastalis + Netinkama nuoroda + Prašome pranešti tai kūrėjams: +\n%s +\n +\nRekomenduojama paleisti programėlę iš naujo. + Ruošiamas atsiuntimas + Importas nepavyko + Taikyti + Eksportuotas failas neegzistuoja + Ruošiamas įkėlimas + Archyvuoti ir įkelti + Ištrinti duomenų bazę iš šio įrengino + Įspėjimas: archyvas bus ištrintas.]]> + Arba nuskanuokite QR kodą + Programėlės prieigos kodas + Prisijungti prie savęs? + Kontaktas dar nėra susijungęs! + Pataisyti pavadinimą į %s? + Laukiama + Duomenų bazės šifravimo slaptafrazė bus atnaujinta ir saugoma raktų saugykloje. + %s yra blogos būsenos]]> + užblokuota administratoriaus + Pilna nuoroda + Atsparu šlamštui ir piktnaudžiavimui + Prašome pranešti tai kūrėjams. + Prašome saugoti slaptafrazę saugiai, jūs NEGALĖSITE pasiekti pokalbių, jei ją prarasite. + Duomenų bazės slaptafrazė yra reikalinga pokalbių atidarymui. + Prašome įvesti praeitą slaptažodį po duomenų bazės atsarginės kopijos atstatymo. Šis veiksmas negali būti atšauktas. + duomenų bazė naujesnė nei programėlė, bet nėra perkėlimo į senesnę versiją: %s + skirtinga migracija programėlėje/duomenų bazėje: %s / %s + Pokalbio archyvas + Grupė neaktyvi + Gauta + Užblokuota administratoriaus + %d valanda + Darbastalis yra užsiėmęs + Kaip tai naudoti + Kaip naudoti savo serverius + Paslėpto profilio slaptažodis + Neteisingas prieigos kodas + Jei įvesite savaiminio susinaikinimo prieigos kodą programėlės atidarymo metu: + Uždrausti siųsti failus ir mediją. + Pagaliau, mes juos turime! 🚀 + Darbastalio adresas + Garso skambutis + Atidaryti pokalbių konsolę + Atidaryti pokalbių profilius + Uždrausti žinučių reakcijas. + ištrynė grupę + Ištrinti %d žinutes? + Patvirtinti slaptažodį + Padėti ragelį + Įrenginio autentifikacija išjungta. Išjungiamas SimpleX užraktas. + %d failas (-ai), kurių bendras dydis yra %s + %d valandos + Prisijungti inkognito režimu + Reikalinga slaptafrazė + Uždrausti siųsti balso žinutes. + EKSPERIMENTINIS + Greitai ir nelaukiant kol siuntėjas prisijungs! + Failai ir medija + Failai ir medija yra draudžiami šioje grupėje. + Failas bus gautas kai jūsų kontaktas užbaigs įkėlimą. + Suteikti leidimus + Grupės profilis yra saugomas narių įrenginiuose, ne serveriuose. + Paprašyta gauti vaizdo įrašą + Labas! +\nPrisijunk prie manęs per SimpleX Chat: %s + Privatumas apibrėžtas iš naujo + Sukurta: %s + jungiamasi (supažindinimo pakvietimas) + Kaip naudoti markdown + Savaiminio susinaikinimo prieigos kodas pakeistas! + Pasilikti nepanaudotą pakvietimą? + Pasilikti + Atviro kodo protokolas ir kodas - bet kas gali paleisti savo serverius. + - pasirinktinai praneškite ištrintiems kontaktams. +\n- profilių vardai su tarpais. +\n- ir daugiau! + Įklijuoti archyvo nuorodą + %d dienos + Nuotrauka + Nuotrauka bus gauta kai jūsų kontaktas yra prisijungęs, palaukite arba patikrinkite vėliau! + Kontaktas ir visos žinutės bus ištrintos - tai negali būti atšaukta! + Išvalyti privačius užrašus? + Išvalyti patvirtinimą + Pokalbių konsolė + Skanuoti QR kodą.]]> + Kaip tai daryti + Jei pasirinksite atmesti, siuntėjui nebus apie tai pranešta. + Nuotrauka + Įvertinti programėlę + jungiamas skambutis… + Sukurkite adresą, kad leistumėte žmonėms prisijungti prie jūsų. + Duomenų bazės ID ir transporto izoliacijos parinktis. + kursyvinis + Atsitiktinė slaptafrazė yra saugoma nustatymuose kaip paprastas tekstas. +\nJūs galite tai pakeisti vėliau. + Pokalbiai veikia + POKALBIO ARCHYVAS + Pokalbiai sustabdyti. Jei jau naudojote šią duomenų bazę kitame įrenginyje, turėtumėte perkelti ją atgal prieš pradedant pokalbius. + užblokavo %s + Keisti gavimo adresą + Sutaisyti prisijungimą + Uždrausti siųsti išnykstančias žinutes. + Grupės sveikinimo žinutė + Grupės moderacija + Blokuokite grupės narius + Savaiminio susinaikinimo prieigos kodas + Greitesnis prisijungimas ir patikimesnės žinutės. + Atkreipkite dėmesį: naudojant tą pačią duomenų bazę dviejuose įrenginiuose, žinučių iššifravimas iš jūsų prisijungimų bus sugadintas saugumo sumetimais.]]> + Netinkamas rodomas vardas! + Keisti prieigos kodą + Įvesti prieigos kodą + Atsakymas į + Savaiminio susinaikinimo prieigos kodas įjungtas! + Keisti savaiminio susinaikinimo rėžimą + prisijungti kaip %s + Duomenų bazės pasendinimas + Prašome patvirtinti, kad tinklo nustatymai yra teisingi šiam įrenginiui. + užblokuotas + pakviestas prisijungti + Inkognito per kontakto adreso nuorodą + Nuorodos atidarymas naršyklėje gali sumažinti ryšio privatumą ir saugumą. Nepatikimos SimpleX nuorodos bus raudonos. + Prašome pasitikrinti, ar naudojate tinkamą nuorodą, arba paprašykite savo kontakto, kad atsiųstų jums dar vieną. + Prieigos kodo įvedimas + Prašome prisiminti arba saugoti tai saugiai - čia nėra būdo atkurti prarastą slaptažodį! + Dabartinis prieigos kodas + Istorija + Gauta žinutė + Failai ir medija yra draudžiami! + Nuotrauka išsaugota į galeriją + Failas bus gautas kai jūsų kontaktas yra prisijungęs, palaukite arba patikrinkite vėliau! + Iš anksto nustatyto serverio adresas + Iš anksto nustatytas serveris + Decentralizuota + Pateikti + Prisijungti inkognito režimu + Pradinė rolė + Gauta: %s + Negrįžtamas žinučių ištrynimas yra draudžiamas šiame pokalbyje. + Uždrausti siųsti išnykstančias žinutes. + Toliau sumažinome akumuliatoriaus naudojimą + Paslėpti pokalbių profiliai + Filtruokite neperskaitytus ir mėgstamus pokalbius. + Raskite pokalbius greičiau + Duomenų bazės perkėlimas yra vykdomas. +\nTai gali užtrukti kelias minutes. + Patvirtinkite savo kredencialus + Nuotrauka išsiųsta + Nuotrauka bus gauta kai jūsų kontaktas užbaigs jos įkėlimą. + Pakeisti gavimo adresą? + nuskanuoti QR kodą vaizdo skambutyje, arba jūsų kontaktas gali pasidalinti pakvietimo nuoroda.]]> + Arba parodykite šį kodą + ICE serveriai (vienas per liniją) + Įdiegti SimpleX Chat terminalui + Ištrinti nuotrauką + Rodyti slaptažodį + Prieigos kodas nustatytas! + Duomenų bazės šifravimo slaptafrazė bus atnaujinta ir saugoma nustatymuose. + Netinkamas perkelimo patvirtinimas + Inkognito režimas apsaugo jūsų privatumą, naudodamas naują atsitiktinį profilį kiekvienam kontaktui. + Skaityti daugiau + Grupės nariai gali siųsti failus ir mediją. + Pilnas vardas: + užbaigti + jungiamasi (paskelbtas) + Uždrausti siųsti tiesioginias žinutes nariams. + Kontaktas turi visapusį šifravimą + Patvirtinti prieigos kodą + Automatiškai priimti nuotraukas + Įjungti savaiminio susinaikinimo prieigos kodą + Prieigos kodas + Savaiminio susinaikinimo prieigos kodas + Programėlės prieigos kodas + Programėlės prieigos kodas pakeičiamas savaiminio susinaikinimo prieigos kodu. + Eksportuoti temą + Tik jūsų kontaktas gali reaguoti į žinutes. + Susikurkite naują proflį darbastalio programoje. 💻 + Prijungtas mobilusis + Prisijungta prie mobiliojo + Įklijuoti darbastalio nuorodą + Darbastalis buvo atjungtas + Darbastalis turi nepalaikomą versiją. Įsitikinkite, kad naudojate tą pačią versiją abiejuose įrenginiuose. + Darbastalis turi neteisingą pakvietimo kodą + Duomenų bazė bus ušifruota ir slaptafrazė bus saugoma raktų saugykloje. + Momentiniai pranešimai + Momentiniai pranešimai! + Momentiniai pranešimai yra išjungti! + profilio nuotraukos vietos ženklas + parodykite QR kodą vaizdo skambutyje, arba pasidalinkite nuoroda.]]> + naudotojo vadove.]]> + Jei negalite susitikti asmeniškai, parodykite QR kodą vaizdo skambutyje, arba pasidalinkite nuoroda. + Įklijuokite nuorodą, kurią gavote prisijungimui prie savo kontakto… + gautas atsakas… + Jungiamas skambutis + Tai gali įvykti kai jūs ar jūsų prisijungimas naudojo seną duomenų bazės atsarginę kopiją. + Jei įvesite šį prieigos kodą programėlės atidarymo metu, visa programėlės informacija bus negrįžtamai pašalinta! + Nustatyti prieigos kodą + Ištrinti failus visiems pokalbių profiliams + Slaptafrazė nebuvo rasta raktų saugykloje, prašome įvesti ją rankiniu būdu. Taip galėjo nutikti, jei atkūrėte programėlės duomenis naudojant atsarginės kopijos įrankį. Jei tai nėra tiesa, prašome susisiekti su kūrėjais. + ištrintas kontaktas + pakviestas + Ištrinta + KONSOLEI + Blokuoti + Protokolui skirtas laikas + numatyta (%s) + Grupės nariai gali negrįžtamai ištrinti išsiųstas žinutes. (24 valandas) + Negrįžtamas žinučių ištrynimas yra draudžiamas šioje grupėje. + Grupė jau egzistuoja! + Blogas darbastalio adresas + Naudoti iš darbastalio mobilioje programėlėje ir nuskanuokite QR kodą.]]> + Darbastalio programėlės versija %s nėra suderinama su šia programėle. + autorius + Kvitai yra išjungti + - prisijunkite prie katalogo tarnybos (BETA)! +\n- pristatymo kvitai (iki 20 narių). +\n- greičiau ir stabiliau. + %1$d žinutė buvo moderuota %2$s + Rastas darbastalis + Prašome saugoti slaptafrazę saugiai, jūs NEGALĖSITE jos pakeisti, jei ją prarasite. + %d grupės įvykiai + profilio nuotrauka + Profilio atnaujinimas bus nusiųstas jūsų kontaktams. + Kvantiškai atsparus visapusis šifravimas + Susisiekti su %1$s? + užblokuotas + Blokuoti visiems + Blokuoti narį? + Archyvuojama duomenų bazė + Patikrinkite savo interneto ryšį ir bandykite vėl + Keisti savaiminio susinaikinimo prieigos kodą + Naujas prieigos kodas + Prieigos kodas nepakeistas! + Duomenų bazės perkelimui, patvirtinkite kad jūs prisimenate jos slaptafrazę. + pakeitė adresą tau + jungiamasi (supažindintas) + Sutaisyti prisijungimą? + Grupės pilnas pavadinimas: + Darbastalio įrenginiai + Tik jūsų kontaktas gali negrįžtamai ištrinti žinutes (jūs galite pažymėti jas ištrynimui). (24 valandas) + Fotoaparatas + Kontaktas neturi visapusio šifravimo + Raskite šį leidimą Android nustatymuose ir suteikite jį rankiniu būdu. + Suteikti nustatymuose + Suteikti leidimą (-us) skambinimui + Momentinis + Atidaryti nustatymus + Laukiamas skambutis + Periodiškai + Pokalbiai sustabdyti + Apsaugoti programėlės ekraną + Kvitų siuntimas išjungtas %d kontaktams + Pokalbiai sustabdyti + jungiamasi (priimtas) + Kontaktas patikrintas + Sukurta %1$s + Duomenų bazės slaptafrazė yra kitokia nei išsaugota raktų saugykloje. + Duomenų bazė bus užšifruota ir slaptafrazė bus saugoma nustatymuose. + Grupės pakvietimas nebegalioja, siuntėjas jį pašalino. + pakvietė %1$s + Buvęs narys %1$s + Blokuoti narį + Blokuoti narį visiems? + Pakeisti grupės rolę? + Keisti rolę + Sutaisyti + Kontaktas nepalaiko sutaisymo. + PING intervalas + Protokolui skirtas laikas per KB + Istorija nėra siunčiama naujiems nariams. + Negrįžtamas žinučių ištrynimas + Uždrausti žinučių reakcijas. + Gauta žinutė + Programėlės duomenų perkėlimas + dienos + valandos + Vaizdas-vaizde skambučiai + Privatūs užrašai + Prisijungimas sustabdytas + Prisijungimas sustabdytas + Prisijungimas nutrauktas + Prisijungimas prie darbastalio yra blogos būsenos + Prisijungti prie darbastalio + Darbastalis yra neaktyvus + Failas buvo ištrintas arba nuoroda netinkama + Importuojamas archyvas + (naujas)]]> + Atidarykite prievadą ugniasienėje + Arba įklijuokite archyvo nuorodą + Prašome pranešti tai kūrėjams: +\n%s + Atsitiktinis + Persikelti iš kito įrenginio naujame įrenginyje ir nuskanuokite QR kodą.]]> + Patvirtinti tinklo nustatymus + Atšaukti perkelimą + Pokalbiai perkelti! + Patvirtinti įkėlimą + Kuriama archyvo nuoroda + Užbaigti perkėlimą + Užbaikite perkėlimą kitame įrenginyje. + Norint tęsti, pokalbiai turi būti sustabdyti. + Arba saugiai pasidalinkite šio failo nuoroda + Išsaugokite paskutinės žinutės juodraštį su priedais. + %s, %s ir %s prisijungė + Šis pokalbis yra apsaugotas su visapusiu šifravimu. + Šis pokalbis yra apsaugotas kvantiškai atspariu visapusiu šifravimu. + Prisijungimas, kurį priėmėte, bus atšauktas! + Bakstelėkite, kad įklijuoti nuorodą + Testuoti serverį + TEMOS SPALVOS + Rodyti lėtus API iškvietimus + Nustoti bendrinti adresą? + Žinučių siuntimo ir programų platforma, apsauganti jūsų privatumą ir saugumą. + Pirma platforma neturinti jokių naudotojų identifikatorių - privati pagal sumanymą. + Nustatyti duomenų bazės slaptafrazę + Sistema + Nustatyti slaptafrazę eksportui + Šis veiksmas negali būti atšauktas - jūsų profilis, kontaktai, žinutės ir failai bus negrįžtamai prarasti. + nustatyti naują kontakto adresą + nustatė naują profilio nuotrauką + Standartinis visapusis šifravimas + Pristatymo kvitai + Nusiųsta + Dėkojame naudotojams - prisidėkite per Weblate! + %s nėra patvirtintas + Lėta funkcija + SimpleX negali veikti fone. Jūs gausite pranešimus tik kai programėlė yra paleista. + Duomenų bazė neveikia tinkamai. Bakstelėkite, kad sužinotumėte daugiau + SimpleX Chat tarnyba + Nustoti bendrinti + Bendrinti su kontaktais + Naujos kartos privatus susirašinėjimas + %s sekundė(s) + Kodas kurį nuskanavote, nėra SimpleX nuorodos QR kodas. + Kontaktas su kurio pasidalinote šia nuoroda NEGALĖS prisijungti! + Įrenginio vardas bus bendrinamas su susietu mobiliuoju klientu. + Ši žinutė bus pažymėta visiems nariams kaip moderuota. + Slaptafrazė bus saugoma nustatymuose kaip paprastas tekstas po jos pakeitimo arba programėlės paleidimo iš naujo. + Serveriai naujiems prisijungimams jūsų dabartiniame pokalbių profilyje + Tekstas kurį įklijavote nėra SimpleX nuoroda. + Šis veiksmas negali būti atšauktas - visi gauti ir siųsti failai ir medija bus ištrinti. Mažos rezoliucijos nuotraukos išliks. + Įjunkite tiesioginiuose pokalbiuose (BETA)! + Ši eilutė nėra prisijungimo nuoroda! + Kad prisijungti, jūsų kontaktas gali nuskanuoti QR kodą arba naudoti nuorodą programėlėje. + Rašyti žinutę gyvai + Pažvaigždinti ant GitHub + Testuoti serverius + Dėkojame naudotojams - prisidėkite per Weblate! + Bakstelėkite mygtuką + Dėkojame naudotojams - prisidėkite per Weblate! + Sistemos autentifikacija + Importuojant įvyko keletas nelemtingų klaidų – daugiau informacijos galite rasti pokalbių konsolėje. + Rodyti kūrėjo parinktis + Bendrinti adresą su kontaktais? + Rodyti vidines klaidas + Naudoti .onion serverius į Ne jei SOCKS tarpinis serveris nepalaiko jų.]]> + Sustabdykite pokalbius, kad eksportuoti, importuoti ar ištrinti duomenų bazę. Jūs negalėsite siųsti ir gauti žinučių kai pokalbiai yra sustabdyti. + Nustatyti kontakto vardą… + Šifravimas veikia ir nauja šifravimo sutartis nėra reikalinga. Tai gali sukelti ryšio klaidų! + Dalintis šia vienkartinio pakvietimo nuoroda + %s yra patvirtintas + Bakstelėkite, kad nuskanuoti + Išjungti? + Skambučiams reikalinga numatytoji žiniatinklio naršyklė. Sukonfigūruokite numatytąją sistemos naršyklę ir pasidalykite daugiau informacijos su kūrėjais. + Mažos grupės (maks. 20) + Pavadinimas + Nustatykite žinutę rodomą naujiems nariams! + Pradėti pokalbį + Šis rodomas vardas netinkamas. Prašome pasirinkti kitą vardą. + Nustatyti kontakto vardą + Nustatyti grupės parinktis + Serveris reikalauja įgaliojimų įkėlimui, patikrinkite slaptažodį + Serveris reikalauja įgaliojimų eilių kūrimui, patikrinkite slaptažodį + Testas nepavyko žingsnyje: %s + Programėlė gali būti uždaryta po 1 minutės būvimo fone. + Pasileidžia periodiškai + Nusiųsta žinutė + Nuotraukos negalima atkoduoti. Bandykite kitą nuotrauką arba susisiekite su kūrėjais. + Vaizdo įrašo negalima atkoduoti. Bandykite kitą vaizdo įrašą arba susisiekite su kūrėjais. + Serverio testas nepavyko! + Kai kurie serveriai nepraėjo testo: + Nusiųsta: %s + Siunčiama per + Nusiųsta žinutė + Antra varnelė kurios mums trūko! ✅ + SimpleX adresas + Siųsti klausimus ir idėjas + Siųsti mums el. laišką + Rodyti konsolę naujame lange + Siųsti nuorodų peržiūras + Sekančios žinutės ID yra neteisingas (mažesnis arba lygus ankstesniam). +\nTaip gali nutikti dėl tam tikros klaidos arba kai ryšys yra sukompromisuotas. + Išjungti + SimpleX + Nustatyti 1 dieną + Dėkojame naudotojams - prisidėkite per Weblate! + Baigėsi skirtasis laikas jungiantis prie darbastalio + Ši galimybė dar nėra palaikoma. Išbandykite sekantį leidimą. + %1$s!]]> + Kad prisijungti su nuoroda + Ši nuoroda nėra tinkama prisijungimo nuoroda! + Šie nustatymai yra jūsų dabartiniam profiliui + Slaptafrazė saugoma nustatymuose kaip paprastas tekstas. + Bakstelėkite, kad prisijungti kaip inkognito + Ši grupė turi daugiau nei %1$d narių, pristatymo kvitai nėra siunčiami. + %s: %s + Su saugiu kvantiškai atspariu protokolu. + Šio įrenginio pavadinimas + Supaprastintas inkognito režimas + Tai yra jūsų SimpleX adresas! + %s, %s ir %d nariai + Bakstelėkite, kad prisijungti + Praleisti narių kvietimą + Bendrinti adresą + Siųsti naujiems nariams iki 100 paskutinių žinučių. + Kvitų siuntimas yra įjungtas %d kontaktams + Pristatymo kvitai yra įjungti %d grupėms + Nustatyti duomenų slaptafrazę + Nustatyti slaptafrazę + Rodyti paskutines žinutes + Sustabdykite pokalbius, kad įgalinti duomenų bazės veiksmus. + PALAIKYKITE SIMPLEX CHAT + Jų galima nepaisyti kontaktų ir grupių nustatymuose. + Šis veiksmas negali būti atšauktas - žinutės išsiųstos ir gautos anksčiau nei pasirinkta bus ištrintos. Tai gali užtrukti kelias minutes. + %s, %s ir %d kiti nariai prisijungė + Pradėti pokalbį? + Bandymas pakeisti duomenų bazės slaptafrazę nebuvo užbaigtas. + TCP ryšiui skirtas laikas + Dėkojame naudotojams - prisidėkite per Weblate! + Sesijos kodas + Šis įrenginys + Tai yra jūsų vienkartinė nuoroda! + %s]]> + Kad leisti mobiliajai programėlei prisijungti prie darbastalio, atidarykite šį prievadą savo ugniasienėje, jei ją turite įgalinę. + Sustabdomi pokalbiai + %s įkeltas + Jūsų pokalbių profiliai + Jūsų pokalbių profilis bus nusiųstas +\njūsų kontaktui + Jūsų duomenų bazė + Jūsų duomenų bazė nėra užšifruota - nustatykite slaptafrazę, kad ją apsaugoti. + Jūsų pokalbių profilis bus nusiųstas grupės nariams + Pakartotinių šifravimo derybų klaida + Pasendinti ir atidaryti pokalbį + Priimant kontakto užklausą įvyko klaida + Klaida + Įvesti serverį rankiniu būdu + Įveskite savo vardą: + Įjungti (palikti nepaisymus + Įjungti visoms grupėms + šifravimas sutartas su %s + reikalingos pakartotinės šifravimo derybos su %s + šifravimas gerai + %d savaitės + %dmėn + Klaida + Aptinkama vietiniame tinkle + %d žinutės užblokuotos administratoriaus + Įvyko klaida kuriant žinutę + Įvyko klaida ištrinant privačius užrašus + Įveskite sveikinimo žinutę… (nebūtina) + Jūs pašalinote %1$s + Įvyko klaida blokuojant narį visiems + %d min + Nesiųsti istorijos naujiems nariams. + visapusiškai užšifruotas + Atsijungta su priežastimi: %s + Jūsų profilis, kontaktai ir gautos žinutės yra saugomi jūsų įrenginyje. + Išnyks + Rodomas vardas negali turėti tarpų. + Nekurti adreso + Atsiuntimas nepavyko + Atsiunčiamas archyvas + visapusiškai užšifruotas balso skambutis + Atsiunčiamos nuorodos detalės + Pasikartojantis rodomas vardas! + šifravimas sutartas + šifravimas gerai su %s + Užbaigti skambutį + Įvyko klaida įjungiant pristatymo kvitus! + Naujas atsitiktinis profilis bus bendrinamas. + Įjungti kvitus? + Įveskite slaptafrazę… + Įveskite grupės pavadinimą: + %d sekundės + Įjungti fotoaparato prieigą + Įjungti kvitus grupėms? + Išjungti visiems + Išjungti kvitus grupėms? + Įjungti visiems + Įveskite sveikinimo žinutę… + Įjungti TCP išlik-gyvas + %d mėnuo + Užšifruoti saugomus failus ir mediją + Visi jūsų kontaktai, pokalbiai ir failai bus saugiai užšifruoti ir dalimis įkelti į sukonfigūruotus XFTP serverius. + Klaida nutraukiant adreso keitimą + visapusiškai užšifruotas vaizdo skambutis + %d žinutės pažymėtos kaip ištrintos + %d žinutės užblokuotos + Pridedant narį (-us) įvyko klaida + Įvyko klaida ištrinant kontakto užklausą + Įvyko klaida ištrinant laukiantį kontakto prisijungimą + %d minutės + Įjungti savaiminį susinaikinimą + Įjungti (palikti grupių nepaisymus) + pakartotinės šifravimo derybos leidžiamos %s + %d savaitė + Redaguoti nuotrauką + leidžiamos pakartotinės šifravimo derybos + reikalingos pakartotinės šifravimo derybos + %d sek. + Pakartotinės šifravimo derybos nepavyko. + Išjungti (palikti grupių nepaisymus) + Bandote pakviesti kontaktą, su kuriuo bendrinote inkognito profilį, į grupę, kurioje naudojate savo pagrindinį profilį + Skirtingi vardai, avatarai ir transporto izoliacija. + Atraskite ir prisijunkite prie grupių + Atsijungti + %dsav. + Jūsų profilis %1$s bus bendrinamas. + Jūs atmetėte grupės kvietimą + Išnyks: %s + ir %d kiti įvykiai + Įveskite tinkamą slaptafrazę. + %ds + Įjunkite skambučius užrakto ekrane per nustatymus. + Išjungti visoms grupėms + Išjungti (palikti nepaisymus) + pasikartojanti žinutė + Užšifruoti vietinius failus + Šiai grupei naudojate inkognito profilį - kad nesidalintumėte pagrindiniu profiliu, neleidžiama kviesti kontaktų + Įveskite slaptažodį paieškoje + Įvyko klaida kuriant nario kontaktą + %dm + %d mėnesiai + Neįjungti + Įjungti + Atjungti darbalaukį? + %s su priežaistimi: %s]]> + Atjungti mobiliuosius + Atrasti per vietinį tinklą + Įvesti slaptafrazę + Įvesti šio įrenginio pavadinimą… + Įvyko klaida atsisiunčiant archyvą + Įvyko klaida ištrinant duomenų bazę + visapusiu šifravimu su tobulu priekiniu slaptumu, išsižadėjimu ir atsigavimu įsilaužus.]]> + kvantiškai atspariu visapusiu šifravimu su tobulu priekiniu slaptumu, išsižadėjimu ir atsigavimu įsilaužus.]]> + Jau turite pokalbių profilį su tuo pačiu rodomu vardu. Pasirinkite kitą vardą. + Jau esate prisijungę prie %1$s. + Įvyko klaida nustatant adresą + Įvyko klaida sinchronizuojant prisijungimą + Siuntėjas gali būti ištrynęs prisijungimo užklausą. + Saugi eilė + leisti SimpleX veikti fone sekančiame dialoge. Kitaip, pranešimai bus išjungti.]]> + Įkelti failą + Veikia kai programėlė yra atidaryta + Negalėjote būti patvirtintas; bandykite vėl. + Galite įjungti SimpleX užraktą per nustatymus. + Išplėsti + Atšaukti failą + Atšaukti failą? + neteisėtas siuntimas + Ieškoti ar įklijuoti SimpleX nuorodą + Jūs turite leisti savo kontaktui siųsti balso žinutes, kad galėtumėte siųsti jas. + (nuskanuokite ar įklijuokite iš iškarpinės) + Priėmėte prisijungimą + Jūs pakvietėte kontaktą + Onion serveriai bus reikalingi ryšiui. + Onion serveriai bus naudojami, kai tik bus. + Išeiti neišsaugant + Jūs kontroliuojate savo pokalbį! + Mes nesaugome jokių jūsų kontaktu ar žinučių (po pristatymo) serveriuose. + Nėra gautų ar išsiųstų failų + Išsaugoti slaptafrazę raktų saugykloje + Atnaujinti duomenų bazės slaptafrazę + atblokavo %s + narys %1$s pasikeitė į %2$s + pašalintas kontakto adresas + pašalinta profilio nuotrauka + atnaujino proflį + saugumo kodas pakeistas + stebėtojas + pakeitėte adresą + Sveikinimo žinutė + Įrašas atnaujintas + %s (dabartinis) + Moderuota: %s + Atstatyti į numatytuosius + Matoma istorija + Tik jūs galite pridėti žinučių reakcijas. + Nauja darbastalio programėlė! + Paieškos laukas priima kvietimo nuorodas. + Su užšifruotais failais ir medija. + Naudokite programėlę skambučio metu. + %s atsisiuntė + Pakartoti atsisiuntimą + Pakartoti importą + Persikelti iš įrenginio + Persikelti į kitą įrenginį + Įvyko klaida išsaugant nustatymus + Įvyko klaida eksportuojant pokalbių duomenų bazę + Įvyko klaida patvirtinant slaptafrazę: + per tarpinį serverį + Jūsų kontaktai gali leisti pilną žinučiu ištrynimą. + Patvirtinkite prisijungimo saugumą + Jūsų profilis bus išsiųstas kontaktui iš kurio gavote šią nuorodą. + Prisijungsite prie visų grupės narių. + Esate prisijungę prie serverio skirto gauti žinutes iš šio kontakto. + Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto (klaida: %1$s). + Bandoma prisijungti prie serverio skirto žinučių gavimui iš šio kontakto. + nėra detalių + SimpleX fono tarnybą - ji naudoja kelis procentus akumuliatoriaus per dieną.]]> + Programėlė periodiškai gauna naujas žinutes — ji sunaudoja kelis procentus baterijos per dieną. Programa nenaudoja tiesioginių pranešimų — duomenys iš jūsų įrenginio į serverius nesiunčiami. + Vienkartinė pakvietimo nuoroda + Neprarasite savo kontaktų, jei vėliau ištrinsite savo adresą. + Kai yra + vaizdo skambutis (neužšifruotas visapusiškai) + Pašalinti slaptafrazę iš raktų saugyklos? + Persikėlimai: %s + Nusiuntėte grupės pakvietimą + atnaujino grupės profilį + atblokavote %s + Išsaugota žinutė + Pašalinti narį? + Atblokuoti visiems + Prisijungimo užklausa bus išsiųsta šiam grupės nariui. + Sumažintas baterijos naudojimas + Naujausia istorija ir patobulintas katalogo botas. + Pristatymo kvitų siuntimas bus įjungtas visiems kontaktams. + Pristatymo kvitų siuntimas bus įjungtas visiems kontaktams visuose matomuose pokalbių profiliuose. + Susieti darbastaliai + Nuskanuoti QR kodą iš darbastalio + %s buvo atjungtas]]> + %1$s.]]> + Galite bendrinti savo adresą kaip nuorodą ar QR kodą - bet kas galės prisijungti prie jūsų. + Atnaujinti transporto izoliacijos režimą? + Išsaugoti automatinio priėmimo nustatymus + Antrinis + Žinučių reakcijos yra draudžiamos šiame pokalbyje. + Vaizdo skambutis + Žinutė bus pažymėta ištrynimui. Gavėjas (-ai) galės atskleisti šią žinutę. + Atšaukti + siuntimas nepavyko + Per daug nuotraukų! + Siųsti išnykstančią žinutę + (kad bendrinti su savo kontaktu) + Kad pradėti naują pokalbį + Pakartoti + Naudoti .onion serverius + Žinutės gyvai + savaitės + Siųsti + Naudoti inkognito profilį + Nauja kontakto užklausa + Gavimo adresas bus pakeistas į kitą serverį. Adreso pakeitimas bus užbaigtas kai siuntėjas prisijungs. + Įvyko klaida įkeliant archyvą + Prisijunkite su savo kredencialais + Įsitikinkite, kad SMP serverių adresai yra tinkamu formatu, atskirose eilutėse ir nesikartojantys. + Įsitikinkite, kad XFTP serverių adresai yra tinkamu formatu, atskirose eilutėse ir nesikartojantys. + Markdown pagalba + Maks. 40 sekundžių, gaunama iš karto. + Žinučių reakcijos + Žinutė bus ištrinta - tai negali būti atšaukta! + Transporto izoliacija + Saugesnės grupės + %s prisijungė + Jūs jau prašėte prisijungimo per šį adresą! + Jūsų kontaktas turi būti prisijungęs, kad užbaigti prisijungimą. +\nJūs galite atšaukti šį prisijungimą ir pašalinti kontaktą (ir vėliau bandyti su nauja nuoroda). + Jūsų kontaktai išliks prisijungę. + Jūsų XFTP serveriai + Prisijungsite kai jūsų prisijungimo užklausa bus patvirtinta, palaukite arba patikrinkite vėliau! + %s yra užsiėmęs]]> + %s yra neaktyvus]]> + mėnesiai + Tikriausiai šis kontaktas ištrynė prisijungimą su jumis. + Nutildyti + Nėra pristatymo informacijos + Dabar administratoriai gali: +\n- ištrinti narių žinutes. +\n- išjungti narius (\"stebėtojas\" rolė) + įj. + Nebemėgti + Nustatymų atnaujinimas perjungs klientą iš naujo prie visų serverių. + Iki 100 paskutinių žinučių bus siunčiamos naujiems nariams. + Naudoti dabartinį profilį + Su nebūtina sveikinimo žinute. + Su sumažintu akumuliatoriaus naudojimu. + Esate kviečiami į grupę + prisijungti prie SimpleX Chat kūrėjų, kad klausti klausimus ir gauti atnaujinimus.]]> + Galite padaryti tai matomą savo SimpleX kontaktams per nustatymus. + Susieto darbastalio parinktys + %1$s.]]> + Pašalinti nutildymą + Laukiama kol mobilusis prisijungs: + Įvyko klaida rodant pranešimą, susisiekite su kūrėjais. + esate kviečiami į grupę + Tik 10 nuotraukų gali būti siunčiamos vienu metu. + Tik 10 vaizdo įrašų gali būti siunčiami vienu metu + Vaizdo įrašas bus gautas kai jūsų kontaktas baigs jo įkėlimą. + Vaizdo įrašas bus gautas kai jūsų kontaktas bus prisijungęs, palaukite arba patikrinkite vėliau! + Laukiama vaizdo įrašo + Laukiama vaizdo įrašo + Derėtis su šifravimu per naujo? + Jūsų kontaktas atsiuntė failą kuris yra didesnis nei dabartinis maksimalus palaikomas dydis (%1$s). + Įrašyti balso žinutę + Derėtis per naujo + Patvirtinti saugumo kodą + Žinutė gyvai! + Nuskanuoti QR kodą + Siųskite žinutę gyvai - ji pasikeis gavėjui (-ams) rašymo metu + Vienkartinė pakvietimo nuoroda + Nuskanuoti saugumo kodą iš jūsų kontakto programėlės. + Prisijungsite prie grupės kai grupės nuorodos savininkas bus prisijungęs, palaukite arba patikrinkite vėliau! + Prisijungsite kai jūsų konktakto įrenginys bus prisijungęs, palaukite arba patikrinkite vėliau! + Atidaryti mobilioje programėlėje, tada bakstelėkite Prisijungti programėlėje.]]> + Naudoti naujiems prisijungimams + Atidaryti mobilioje programėlėje mygtuko.]]> + Įvyko klaida atveriant naršyklę + paslaptis + Pranešimai nustos veikti iki tol kol paleisite programėlę iš naujo + Galite naudoti markdown, kad formatuoti žinutes: + PALEISTI POKALBIUS + Naudoti iš darbastalio + Sveikinimo žinutė yra per ilga + Žinučių reakcijos + Persikelti į kitą įrenginį su QR kodu. + Persikėlimas baigtas + turite nenaudoti tos pačios duomenų bazės dviejuose įrenginiuose.]]> + Įspėjimas: pokalbių paleidimas keliuose įrenginiuose vienu metu nėra palaikomas ir sukels žinučių pristatymo problemas + Patvirtinti duomenų bazės slaptafrazę + Patvirtinti slaptafrazę + Nutildyti + gauti žinutes, jūsų kontaktai - serverius kuriuos naudojate siųsti jiems žinutes.]]> + Tarpinis serveris apsaugo jūsų IP adresą, bet jis gali stebėti skambučio trukmę. + nėra visapusio šifravimo + Naujas duomenų bazės archyvas + Jūs turite įvesti slaptafrazę kas kartą kai programėlė pasileidžia - tai nėra saugoma įrenginyje. + Jūs pasidalinote netinkamu failo keliu. Praneškite šią problemą programėlės kūrėjams. + siųsti tiesioginę žinutę + Jūs neturite pokalbių + Nėra filtruotų pokalbių + Vietinis vardas + Derėtis su šifravimu per naujo + pažymėta ištrinta + GYVAI + moderuota + Nebent jūsų kontaktas ištrynė šį prisijungimą arba nuoroda jau buvo panaudota, tai gali būti klaida - prašome ją pranešti. +\nKad prisijungti, paprašykite savo kontakto sukurti kitą prisijungimo nuorodą ir patikrinkite, kad turite stabilų interneto ryšį. + Programėlės akumuliatoriaus naudojimas / Neapribotas programėlės nustatymuose.]]> + Programėlės akumuliatoriaus naudojimas / Neapribotas programėlės nustatymuose.]]> + Nėra foninių skambučių + Jums reikės autentifikuotis kai paleidžiate programėlę arba pratęsiate jos naudojimą po 30 sekundžių fone. + Nėra istorijos + esate stebėtojas + (saugo tik grupės nariai) + Jūsų SimpleX adresas + Nuskanuoti serverio QR kodą + Reikalingi + Onion serveriai bus naudojami, kai tik bus. + Onion serveriai nebus naudojami. + Savaiminis susinaikinimas + Senas duomenų bazės archyvas + Jūsų pokalbių duomenų bazė bus IŠTRINTA ir PAKEISTA su importuojama. +\nŠis veiksmas negali būti atšauktas - jūsų profilis, kontaktai, žinutės ir failai bus negrįžtamai prarasti. + Išsaugoti slaptafrazę nustatymuose + Pranešimai bus pristatyti tik iki tol kol programėlė bus sustabdyta! + Galite bendrinti nuorodą ar QR kodą - bet kas galės prisijungti prie grupės. Neprarasite grupės narių jei vėliau tai ištrinsite. + Įrašas atnaujintas: %s + Gaunama per + Kad apsaugoti laiko juostą, nuotraukos/balso failai naudoja UTC. + Žinučių pristatymo kvitai! + - balso žinutės iki 5 minučių. +\n- pasirinktinis laikas išnykimui. +\n- istorijos redagavimas. + Padarykite, kad viena žinutė išnyktų + Net jei tai išjungta pokalbyje. + moderuota %s + Rodinys sudužo + pasidalinote vienkartine nuoroda + pasidalinote vienkartine nuoroda inkognito režimu + Kad apsaugoti savo informaciją, įjunkite SimpleX užraktą. +\nJūs būsite prašomi užbaigti autentifikaciją prieš įjungiant šią galimybę. + Laukiama nuotraukos + Siųsti tiesioginę žinutę, kad prisijungti + Laukiama nuotraukos + Kai asmenys išsiunčia užklausą prisijungti, galite ją priimti arba atmesti. + Galite peržiūrėti pakvietimo nuorodą vėl, prisijungimo detalėse. + Markdown žinutėse + Norėdami patvirtinti visapusį šifravimą su savo kontaktu, palyginkite (ar nuskanuokite) kodą ant savo įrenginių. + XFTP serveriai + Onion serveriai bus reikalingi ryšiui. +\nAtkreipkite dėmesį: negalėsite prisijungti prie serverių be .onion adreso. + Išsaugoti nustatymus? + Norėdami atskleisti paslėptą profilį, įveskite visą slaptažodį į paieškos laukelį puslapyje Jūsų pokalbių profiliai. + Nepasirinkti jokie kontaktai + Galite bendrinti šią nuorodą su savo kontaktais, kad leistumėte jiems prisijungti prie %s. + pasiūlė %s + pasiūlė %s: %2s + Naujas rodomas vardas: + pakeitėte adresą %s + Išplėsti rolių pasirinkimą + %1$s.]]> + jei SimpleX neturi naudotojų identifikatorių, kaip jis gali pristatyti žinutes?]]> + dviejų sluoksnių visapusiu šifravimu.]]> + Kad apsaugoti privatumą, vietoj naudotojų ID naudojamų visose kitose platformose, SimpleX turi identifikatorius žinučių eilėms, skirtingus kiekvienam jūsų kontaktui. + Žinutės juodraštis + Paleisti iš naujo + narys + gauta, uždrausta + Tik jūs galite ištrinti žinutes negrįžtamai (jūsų kontaktas gali pažymėti jas ištrynimui). (24 valandas) + Transporto izoliacija + Įjunkite inkognito režimą prisijungimo metu. + %s yra dingęs]]> + Pakartoti prisijungimo užklausą? + Įvyko klaida rodant turinį + Įvyko klaida rodant žinutę + Įvyko klaida įkeliant detales + Siuntėjas atšaukė failo perdavimą + Moderuoti + nori prisijungti prie jūsų! + nuorodos peržiūros nuotrauka + Onion serveriai nebus naudojami. + Atnaujinti .onion serverių nustatymą? + Kai programėlė yra paleista + Užrakto režimas + Galite paleisti pokalbius per programėlės nustatymus/ duomenų bazę arba paleisdami programėlę iš naujo. + pašalino jus + Moderuota + NARYS + %s %s + nėra teksto + Meniu ir įspėjimai + Susiekite mobiliasias ir darbastalio programėles! 🔗 + Susieti mobilieji + Patvirtinti prisijungimą + Nuskanuoti iš mobiliojo + Patvirtinkite kodą mobiliąjame + %s buvo atjungtas]]> + Tik vietinio profilio duomenys + Pakartoti prisijungimo užklausą? + Jau yra prisijungiama su šia vienkartine nuoroda! + Nauja nario rolė + Patvirtinti prisijungimus + Laukiama darbastalio… + Funkcijos vykdymas trunka per ilgai: %1$d sekundės: %2$s + %s ir %s + Atnaujinti + Įsitikinkite, kad WebRTC ICE serverių adresai yra tinkamu formatu, atskirose eilutėse ir nesikartojantys. + Galite sukurti tai vėliau + Nėra pasirinkto pokalbio + nežinoma būsena + pašalintas + nežinoma + Narys bus pašalintas iš grupės - tai negali būti atšaukta! + Sukurkite privatų prisijungimą + Tarpinis serveris naudojamas tik jei to reikia. Kita šalis gali stebėti jūsų IP adresą. + Mikrofonas + Persikelti iš kito įrenginio + Naudoti atsiktinę slaptafrazę + lygiaverčiai mazgai + Pašalinti slaptafrazę iš nustatymų? + SIŲSTI PRISTATYMO KVITUS PAS + Pristatymo kvitai yra išjungti %d grupėms + Turite naudoti pačią naujausią pokalbių duomenų bazės versiją TIK viename įrenginyje, kitaip galite nebegauti žinučių iš kai kurių kontaktų. + Nauja slaptafrazė… + pašalino %1$s + %s ir %s prisijungė + Pasirinkti kontaktus + Jūs prisijungėte prie šios grupės. Prisijungiama prie kviečiančio grupės nario. + Nėra kontaktų pridėjimui + Esate kviečiami į grupę. Ateikite, kad prisijungti prie grupės narių. + užblokavote %s + pakeitėte rolę sau į %s + pakeitėte rolę %s į %s + Įvyko klaida pašalinant narį + Įvyko klaida siunčiant kvietimą + Žinutės nuo %s bus rodomos! + Pašalinti narį + Išsaugoti sveikinimo žinutę? + Atblokuoti + Atblokuoti narį + Atblokuoti narį? + Atblokuoti narį visiems? + Sveikinimo žinutė + Padarykite profilį privatų! + Įsitikinkite, kad failas turi teisingą YAML sintaksę. Eksportuokite temą, kad gautumėte temos failo struktūros pavyzdį. + Žinučių reakcijos yra draudžiamos šioje grupėje. + Gavėjai mato pakeitimus kai juos rašote. + Vistiek gausite skambučius ir pranešimus iš nutildytų profilių kai jie yra aktyvūs. + Nutildyta kai neaktyvu! + Kai jūs bendrinate inkognito profilį su kažkuom, šis profilis bus naudojamas grupėms į kurias jie jus pakviečia. + Galite paslėpti ar nutildyti naudotojo profilį - laikykite jį, kad atidaryti meniu. + sekundės + Pasirinkti + Kad paslėptumėte nenorimas žinutes. + minutės + Galite įjungti vėliau per nustatymus + Galite įjungti juos vėliau per programėlės privatumo ir saugumo nustatymus. + Susieti mobilujį + Paleisti pokalbius iš naujo + Persikelti į čia + Persikėliama + %s turi nepalaikomą versiją. Įsitikinkite, kad naudojate tą pačią versiją abiejuose įrenginiuose]]> + Naujas mobilusis įrenginys + Nėra prisijungusių mobiliųjų + Atsieti + Atsieti darbastalį? + Patvirtinkite kodą darbastalyje + Jau jungiatės prie grupės su šia nuoroda. + Galite bandyti dar kartą. + Pakartoti įkėlimą + Įkėlimas nepavyko + Įkeliamas archyvas + Galite bandyti dar kartą. + Bluetooth + visi nariai + Ausinė + Ausinės + administratoriai + Persiųsta + Atsisiųsti + Persiųsti + Failai ir medija neleidžiami + persiųsta + Leisti siųsti SimpleX nuorodas + Grupės nariai gali siųsti SimpleX nuorodas. + Įjungta: + Mobilusis + Persiųskite ir išsaugokite žinutes + Persiųsta iš + Persiųsti žinutę… + Žinutės šaltinis išlieka privatus. + Patikimesnis tinklo ryšys. + Tinklo ryšys + Garsai skambučio metu + Lietuviškas UI + Gavimas vienu metu + Garsiakalbis + Tinklo valdymas + išsaugota + išsaugota iš %s + Išsaugota + Balso žinutės neleidžiamos + WiFi + SimpleX nuorodos + Drausti SimpleX nuorodų siuntimą + SimpleX nuorodos yra uždraustos šioje grupėje. + savininkai + Laidinis internetas + Kitas + Nėra interneto ryšio + Išsaugota iš + Gavėjas(-ai) negali matyti kas šią žinutę išsiuntė. + SimpleX nuorodos neleidžiamos + Kai jungiami garso ir vaizdo skambučiai. + Bus įjungta tiesioginiuose pokalbiuose! \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index ecfaf72a18..ab10a01a66 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -1714,4 +1714,57 @@ Wachtwoord instellen Deze chat wordt beschermd door quantum bestendige end-to-end codering. Fout bij weergeven van melding. Neem contact op met ontwikkelaars. + Zoek deze toestemming in de Android-instellingen en verleen deze handmatig. + Verlenen in instellingen + Machtigingen verlenen + Verleen toestemming(en) om te bellen + Microfoon + Camera + Camera en microfoon + Open instellingen + Mobiel + doorgestuurd + Netwerkverbinding + Geen netwerkverbinding + Ander + Wifi + Bekabeld Ethernet + Bestanden en media niet toegestaan + SimpleX-links zijn niet toegestaan + Spraakberichten niet toegestaan + SimpleX-links + Verbied het verzenden van SimpleX-links + beheerders + alle leden + eigenaren + SimpleX-links zijn in deze groep verboden. + Ingeschakeld voor + Sta het verzenden van SimpleX-links toe. + Groepsleden kunnen SimpleX-links verzenden. + opgeslagen + opgeslagen van %s + Doorsturen + Doorgestuurd + Ontvanger(s) kunnen niet zien van wie dit bericht afkomstig is. + Opgeslagen van + Bericht doorsturen… + Downloaden + Doorgestuurd vanuit + Opgeslagen + Oortje + Koptelefoon + Luidspreker + Bluetooth + Berichtbron blijft privé. + Netwerkbeheer + Bij het verbinden van audio- en video-oproepen. + Wordt ingeschakeld in directe chats! + Gelijktijdig ontvangen + Berichten doorsturen en opslaan + Geluiden tijdens het bellen + Litouwse gebruikersinterface + Betrouwbaardere netwerkverbinding. + Vorm profiel afbeeldingen + Profiel afbeeldingen + Vierkant, cirkel of iets daartussenin. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 04e40e73cb..d667126730 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -21,8 +21,8 @@ k NA ŻYWO zaznaczona jako usunięta - moderowany - moderowany przez %s + zmoderowane + zmoderowane przez %s wysyłanie plików nie jest jeszcze obsługiwane odbieranie plików nie jest jeszcze obsługiwane Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu. @@ -303,8 +303,8 @@ Nieprawidłowy adres serwera! Pomoc markdown Markdown w wiadomościach - Wstępnie ustawiony serwer - Wstępnie ustawiony adres serwera + Predefiniowany serwer + Adres predefiniowanego serwera Oceń aplikację Zapisane serwery WebRTC ICE zostaną usunięte. Zapisz serwery @@ -435,7 +435,7 @@ Można to później zmienić w ustawieniach. Nawiąż prywatne połączenie jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości\?]]> - 2-warstwowego szyfrowania end-to-end.]]> + dwuwarstwowego szyfrowania end-to-end.]]> Okresowo Prywatne powiadomienia repozytorium GitHub.]]> @@ -449,8 +449,8 @@ Wklej link, który otrzymałeś połączenie wideo (bez szyfrowania e2e) Akceptuj - zaszyfrowane e2e połączenie audio - zaszyfrowane e2e połączenie wideo + połączenie audio zaszyfrowane e2e + połączenie wideo zaszyfrowane e2e Ignoruj Odrzuć Zawsze używaj przekaźnika @@ -477,7 +477,7 @@ kontakt posiada szyfrowanie e2e kontakt nie posiada szyfrowania e2e zduplikowana wiadomość - zaszyfrowany e2e + zaszyfrowane e2e Włącz połączenia z ekranu blokady poprzez Ustawienia. Odwróć aparat Rozłącz się @@ -494,7 +494,7 @@ przez przekaźnik Wideo wyłączone Wideo włączone - %1$d wiadomość(i) pominięta(e) + %1$d pominięta(ych) wiadomość(i) Może to nastąpić, gdy: \n1. Wiadomości wygasły w wysyłającym kliencie po 2 dniach lub na serwerze po 30 dniach. \n2. Odszyfrowanie wiadomości nie powiodło się, ponieważ Ty lub Twój kontakt użyliście starej kopii zapasowej bazy danych. @@ -904,7 +904,7 @@ Dodaj serwery, skanując kody QR. Dodaj wiadomość powitalną Wszystkie czaty i wiadomości zostaną usunięte - nie można tego cofnąć! - Pozwól + Zezwól zawsze Zawsze włączona Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny) @@ -997,7 +997,7 @@ Zatrzymaj czat, aby wyeksportować, zaimportować lub usunąć bazę danych czatu. Podczas zatrzymania chatu nie będzie można odbierać ani wysyłać wiadomości. Niektóre serwery nie przeszły testu: Dziękujemy za zainstalowanie SimpleX Chat! - Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków. + Wiadomość zostanie oznaczona jako zmoderowana dla wszystkich członków. Rola zostanie zmieniona na \"%s\". Wszyscy w grupie zostaną powiadomieni. Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną. Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online. @@ -1215,7 +1215,7 @@ Otrzymano wiadomość Wyślij wiadomość Usunięto o - Moderowany o + Zmoderowane o Otrzymane o: %s Rekord zaktualizowany o Wysłano o @@ -1226,7 +1226,7 @@ Znika o Znika o: %s Info - Moderowany o: %s + Zmoderowane o: %s Historia Otrzymane o %s (aktualny) @@ -1349,7 +1349,7 @@ Prośba o połączenie zostanie wysłana do tego członka grupy. Połączyć bezpośrednio\? Użyj obecnego profilu - Pozwól + Zezwól Wyłącz powiadomienia Otwórz ustawienia aplikacji Brak rozmów w tle @@ -1639,8 +1639,8 @@ Połączenie wideo Błąd podczas otwierania przeglądarki Do połączeń wymagana jest domyślna przeglądarka. Proszę skonfigurować domyślną przeglądarkę systemową, i podzielić się informacją z twórcami. - Ten czat jest chroniony przez kwantowo odporne szyfrowanie end-to-end. - szyfrowanie end-to-end z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu.]]> + Ten czat jest chroniony przez szyfrowanie e2e odporne na ataki kwantowe. + szyfrowanie end-to-end z perfect forward secrecy, zaprzeczalnością i odzyskiwaniem bezpieczeństwa po kompromitacji.]]> Otwórz ekran migrowania Zmigruj z innego urządzenia Ustaw hasło @@ -1650,7 +1650,7 @@ Bezpieczniejsze grupy Używaj aplikacji podczas połączenia. Połączenia obraz-w-obrazie - Administratorzy mogą blokować członka dla wszystkich. + Administratorzy mogą zablokować członka dla wszystkich członków. Nieprawidłowy link Zmigruj tutaj Migrowanie @@ -1686,7 +1686,7 @@ Potwierdź, że pamiętasz hasło do bazy danych, aby ją zmigrować. Zweryfikuj hasło bazy danych Sprawdź swoje połączenie z internetem i spróbuj ponownie - Wszystkie twoje kontakty, konwersacje i pliki będą bezpiecznie szyfrowane i wgrywane w kawałkach do skonfigurowanych przekaźników XFTP. + Wszystkie twoje kontakty, konwersacje i pliki zostaną bezpiecznie zaszyfrowane i wgrane w kawałkach do skonfigurowanych przekaźników XFTP. Archiwizuj i prześlij Uwaga: używanie tej samej bazy danych na dwóch urządzeniach zepsuje odszyfrowywanie wiadomości twoich połączeń, jako zabezpieczenie.]]> Ostrzeżenie: archiwum zostanie usunięte.]]> @@ -1713,6 +1713,60 @@ kwantowo odporne szyfrowanie end-to-end z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu.]]> Zmigruj do innego urządzenia przez kod QR. Proszę potwierdzić, że ustawienia sieciowe są prawidłowe dla tego urządzenia. - kwantowo odporne szyfrowanie e2e - Kwantowo odporne szyfrowanie + szyfrowanie e2e odporne na ataki kwantowe + Szyfrowanie odporne na ataki kwantowe + Znajdź to uprawnienie w ustawieniach Androida i przydziel je manualnie. + Zezwól w ustawieniach + Przydziel uprawnienia + Aparat + Aparat i mikrofon + Błąd podczas pokazywania powiadomienia, skontaktuj się z deweloperami. + Przydziel uprawnienia, by wykonywać połączenia + Mikrofon + Otwórz ustawienia + przekazane dalej + Połączenie z siecią + Pliki i multimedia nie są dozwolone + Linki SimpleX są niedozwolone + Wiadomości głosowe są niedozwolone + Włączony dla + właściciele + Linki SimpleX są zablokowane na tej grupie. + Inne + WiFi + Połączenie ethernet (po kablu) + administratorzy + wszyscy członkowie + Zezwól na wysyłanie linków SimpleX. + Sieć komórkowa + Członkowie grupy mogą wysyłać linki SimpleX. + Brak połączenia z siecią + Zabroń wysyłania linków SimpleX + Linki SimpleX + Przekazane dalej + Przekazane dalej od + Odbiorca/y nie mogą zobaczyć od kogo jest ta wiadomość. + Zapisane od + Pobierz + Przekaż dalej + Przekaż wiadomość… + Zapisane + zapisane + zapisane od %s + Bluetooth + Przesyłaj dalej i zapisuj wiadomości + Słuchawki douszne + Słuchawki + Głośnik + Podczas łączenia połączeń audio i wideo. + Dźwięki w rozmowie + Zdjęcia profilowe + Konkurencyjne odbieranie + Litewski interfejs użytkownika + Bardziej niezawodne połączenia sieciowe. + Zarządzenie sieciowe + Kształtuj obrazy profilowe + Kwadrat, okrąg lub cokolwiek pomiędzy. + Źródło wiadomości pozostaje prywatne. + Zostanie włączone w czatach bezpośrednich! \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 3e06927ddc..b6f268e6f1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -52,7 +52,7 @@ Toate conversațiile și mesajele vor fi șterse - operațiunea este definitivă! Toate mesajele vor fi șterse - operațiunea este definitivă! Permite reacții la mesaje doar dacă și contactul tău le permite. - Permite ștergerea ireversibila a mesajelor trimise. (24 ore) + Permite ștergerea ireversibilă a mesajelor trimise. (24 ore) Permite trimiterea de fișiere și media. Toate datele sunt șterse când este introdusă. Toți membrii grupului vor rămâne conectați. @@ -69,4 +69,90 @@ Permite trimiterea de mesaje efemere. Permite trimiterea de mesaje vocale. Permite mesaje efemere doar dacă le permite contactul tău. + Permite contactelor tale să trimită mesaje care dispar. + Aspect + Versiunea aplicației + toți membrii + Permite trimiterea linkurilor SimpleX. + administratori + Administratorii pot bloca un membru pentru toți. + întotdeauna + Versiunea aplicației: v%s + Permite contactelor tale să șteargă ireversibil mesajele trimise. (24 de ore) + Permite contactelor tale să adauge reacții la mesaje. + Permite contactelor tale să te apeleze. + Permite contactelor tale să trimită mesaje vocale. + Toate contactele tale vor rămâne conectate. + Anulare + Camera indisponibilă + Camera și microfon + Atenție: arhiva va fi ștearsă.]]> + Anulează previzualizarea fișierului + Un profil de conversație gol cu numele furnizat este creat, și aplicația se deschide ca de obicei. + Se conectează deja! + Aplicația poate primi notificări doar când rulează, niciun serviciu în fundal nu va fi lansat. + Se alătură deja grupului! + Mereu pornit + Un nou profil aleatoriu va fi distribuit. + Android Keystore este folosit pentru a stoca în siguranță fraza de acces - permite serviciului de notificare să funcționeze. + și %d alte evenimente + Răspunde la apel + Android Keystore va fi folosit pentru a stoca în siguranță fraza de acces după ce repornești aplicația sau schimbi fraza de acces - va permite primirea notificărilor. + APLICAȚIE + Creează grup + Apeluri audio și video + Arhivează și încarcă + Bază de date de arhivare + Se creează un link de arhivare + Creează un link de invitare unic. + Acceptă automat imagini + Apel audio + Apel audio + Audio oprit + PICTOGRAMĂ APLICAȚIE + Cod de acces aplicație + Creează grup secret + Creează coadă + Serviciul în fundal rulează mereu - notificările vor fi afișate imediat ce sunt disponibile. + Autentificare + Autentificare eșuată + Codul de acces curent + Autentificare indisponibilă + Atașează + Înapoi + Creează grup secret + Se creează link… + apel audio (necriptat e2e) + Creează fișier + Adaugă contact: pentru a crea un nou link de invitare, sau a te conecta printr-un link pe care l-ai primit.]]> + Autentificare anulată + Cod de acces aplicație este înlocuit cu cod de acces de autodistrugere. + Apeluri audio/video + " +\nDisponibil în v5.1" + Cod de acces aplicație + Migrare date aplicație + Eroare critică + Aplică + Creează profil + Creează profil + Acceptă automat cererile de contactare + personalizat + În prezent dimensiunea maximă pentru fișiere este %1$s. + Creează adresă SimpleX + Acceptare automată + Creează-ți profilul + Apeluri audio și video + Audio pornit + Frază de acces curentă… + Creează link pentru grup + Creează link + creator + Fundal + Creează profil nou în aplicația desktop. 💻 + Aplicația criptează fișierele locale noi (cu excepția videoclipurilor). + autor + Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate. + Apelurile audio/video sunt interzise. + (prezent) \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index f855bb040b..a71638eae0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -1798,4 +1798,57 @@ \nЭто может занять несколько минут. Ошибка открытия браузера Для звонков требуется веб-браузер по умолчанию. Пожалуйста, настройте браузер по умолчанию в системе и поделитесь дополнительной информацией с разработчиками. + Другaя сеть + WiFi + Проводная сеть + Мобильная сеть + Интернет-соединение + Нет интернет-соединения + Ошибка при показе уведомления, свяжитесь с разработчиками. + Камера + Камера и микрофон + Найдите это разрешение в настройках Android и добавьте его вручную. + Разрешить в настройках + Дать разрешение + Дать разрешение для совершения звонков + Микрофон + Открыть настройки + Bluetooth + Одновременный приём + Верхний динамик + Наушники + Громкоговоритель + Звуки во время звонков + Более надежное соединение с сетью. + Статус сети + сохранено + сохранено из %s + Переслано + Переслано из + Получатели не видят от кого это сообщение. + переслано + Файлы и медиа не разрешены + Ссылки SimpleX не разрешены + Голосовые сообщения не разрешены + Ссылки SimpleX + Разрешить отправлять ссылки SimpleX. + Запретить отправку ссылок SimpleX + Члены группы могут отправлять ссылки SimpleX + админы + все члены + владельцы + Загрузить + Сохранено + Сохранено из + Включено для + Переслать + Переслать и сохранить сообщение + Ссылки SimpleX запрещены в этой группе. + Переслать сообщение… + Литовский интерфейс + Источник сообщения остаётся конфиденциальным. + Во время соединения аудио и видео звонков. + Форма картинок профилей + Квадрат, круг и все, что между ними. + Будет включено в прямых разговорах! \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 4464e323f7..9ae8707c11 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -1530,7 +1530,7 @@ Yerel ağ üzerinden keşfedilebilir Kayıt güncellendi Sunucu kullan - Şuradan alındı: %s + Alınma tarihi: %s Kişi Ekle - dizin hizmetine bağlan \n(BETADA!) @@ -1652,7 +1652,7 @@ Doğrudan sohbetlerde etkinleştir (BETA)! Kuantuma dayanıklı şifreleme Yöneticiler bir üyeyi tamamen engelleyebilirler. - Uygulama veri göçü. + Uygulama verisi taşıma QR kodu aracılığıyla başka bir cihaza geçiş yapın. Resim içinde resim çağrıları Daha güvenli gruplar @@ -1716,4 +1716,58 @@ kullanmamalısınız.]]> Bir kez daha deneyebilirsiniz. Başka bir cihazdan taşı\'yı seçin ve QR kodunu tarayın.]]> + Bu izni Android ayarlarında bulun ve manuel olarak izin verin. + İzinleri verin + Arama yapmak için izin(ler)i verin + Mikrofon + Ayarları aç + Kamera + Kamera ve mikrofon + Bildirim gösterilirken hata oluştu, geliştiricilerle irtibata geçin. + Ayarlarda izin verin + Bluetooth + sahipler + SimpleX bağlantısı gönderimini yasakla + Eşzamanlılık alınıyor + SimpleX bağlantıları + SimpleX bağlantıları bu grupta yasaklandı + Kulaklık + Hoparlör + Kulaklıklar + Doğrudan sohbetlerde etkinleştirilecektir! + Ağ yönetimi + Litvanya Kullanıcı Arayüzü + Diğer + kaydedildi + %s tarafından kaydedildi + İletildi + Kaydedildi + İndir + Şuradan iletildi + Mesaj ilet… + Alıcı(lar) bu mesajın kimden geldiğini göremez. + SimpleX bağlantılarına izin verilmiyor + Dosyalar ve medyaya izin verilmiyor + Sesli mesajlara izin verilmiyor + Grup üyeleri SimpleX bağlantıları gönderebilir. + iletildi + Ağ bağlantısı + SimpleX bağlantıları göndermesine izin ver. + Ağ bağlantısı yok + İlet + Mesajları ilet ve kaydet + yöneticiler + Hücresel Veri + Şunlar için etkinleştirildi + bütün üyeler + Arama içi sesler + Tarafından kaydedildi + Mesaj kaynağı gizli kalır. + Daha güvenilir ağ bağlantısı. + WiFi + Sesli ve görüntülü aramalara bağlanırken. + Kablolu ethernet + Profil resimleri + Profil resimlerini şekillendir + Kare,daire, veya aralarında herhangi bir şey. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index d502c35e9e..939071da2b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -96,4 +96,80 @@ ỨNG DỤNG Di chuyển dữ liệu ứng dụng Sao lưu dữ liệu ứng dụng + Mật mã ứng dụng đã được thay thế bằng mật mã tự hủy. + Ứng dụng mã hóa các tệp cục bộ mới (trừ video). + Áp dụng + Mật mã ứng dụng + BIỂU TƯỢNG ỨNG DỤNG + Mật mã ứng dụng + Phiên bản ứng dụng: v%s + Phiên bản ứng dụng + Tiếng Ả Rập, tiếng Bungari, tiếng Phần Lan, tiếng Do Thái, tiếng Thái và tiếng Ukraina - trân thành gửi lời cảm ơn tới các tình nguyện viên dịch thuật và Weblate. + Lưu trữ cơ sở dữ liệu + Lưu trữ và tải lên + Mở âm + Yêu cầu để nhận video + Gắn + Cuộc gọi thoại + cuộc gọi thoại (không được mã hóa đầu cuối) + Tắt âm + Yêu cầu để nhận hình ảnh + cho mỗi hồ sơ trò chuyện bạn có trong ứng dụng.]]> + Một kết nối TCP riêng biệt (và thông tin xác thực SOCKS) sẽ được sử dụng cho từng liên hệ và thành viên nhóm. +\nXin lưu ý: nếu bạn có nhiều kết nối, mức tiêu thụ pin và lưu lượng truy cập của bạn có thể cao hơn đáng kể và một số kết nối có thể không thành công. + cuộc gọi thoại + Các cuộc gọi thoại và video + Các cuộc gọi thoại và video + Các cuộc gọi thoại/video + tất cả các thành viên + Các cuộc gọi thoại/video bị cấm. + Cho phép gửi liên kết SimpleX. + quản trị viên + tác giả + Quá trình xác thực đã bị hủy bỏ + Xác thực + Quá trình xác thực không có sẵn + Quá trình xác thực đã thất bại + Phông nền + Hàm băm tin nhắn xấu + ID tin nhắn xấu + ID tin nhắn xấu + Dịch vụ nền luôn luôn chạy - thông báo sẽ được hiển thị ngay khi nhận được tin nhắn. + Tự động chấp nhận yêu cầu liên hệ + Địa chỉ máy tính xấu + Thêm liên hệ: để tạo liên kết mời mới, hoặc kết nối qua liên kết bạn nhận được.]]> + hàm băm tin nhắn xấu + " +\nCó sẵn ở v5.1" + Tự động chấp nhận + Quay về + Tự động chấp nhận hình ảnh + Bluetooth + Nhóm tốt hơn + Chặn thành viên nhóm + đã chặn %s + Tin nhắn tốt hơn + Xin lưu ý: việc sử dụng cùng một cơ sở dữ liệu trên hai thiết bị sẽ phá vỡ quá trình giải mã tin nhắn từ các kết nối của bạn, như một biện pháp bảo vệ.]]> + Tạo nhóm: để tạo một nhóm mới.]]> + Tốt nhất cho pin. Bạn sẽ chỉ nhận được thông báo khi ứng dụng đang chạy (KHÔNG có dịch vụ nền).]]> + Xin lưu ý: bạn sẽ KHÔNG thể khôi phục hoặc thay đổi passphrase nếu bạn làm mất nó.]]> + Bị chặn bởi quản trị viên + Chặn thành viên + đã chặn + bị chặn bởi quản trị viên + đã chặn + Tính năng tối ưu hóa pin đang được kích hoạt, tắt dịch vụ nền và các yêu cầu định kỳ về tin nhắn mới. Bạn có thể kích hoạt lại chúng thông qua cài đặt. + Có thể tắt thông qua cài đặt - thông báo vẫn sẽ được hiển thị khi ứng dụng đang chạy.]]> + Chặn thành viên? + Cả bạn và liên hệ của bạn đều có thể xóa tin nhắn đã gửi mà không thể hoàn tác. (24 tiếng) + Chặn tất cả + Chặn thành viên cho tất cả? + Chặn + Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn thoại. + Cả bạn và liên hệ của bạn đều có thể thực hiện cuộc gọi. + in đậm + Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn tự xóa. + Cả bạn và liên hệ của bạn đều có thể thả cảm xúc tin nhắn. + Xin lưu ý: relay tin nhắn và tệp được kết nối thông qua SOCKS proxy. Các cuộc gọi và bản xem trước liên kết sử dụng kết nối trực tiếp.]]> + Tốt cho pin. Dịch vụ nền kiểm tra tin nhắn 10 phút một lần. Bạn có thể bỏ lỡ các cuộc gọi hoặc tin nhắn khẩn cấp.]]> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 3298da5097..536207892d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1715,4 +1715,58 @@ 你可以再试一次。 你可以再试一次。 请注意: 作为安全保护措施,在两部设备上使用同一数据库会破坏解密来自你联系人的消息。]]> + 显示通知出错,请联系开发者。 + 在 Android 系统设置中找到此权限并手动授予权限。 + 在系统设置中授予 + 要进行通话请授予一项或多项权限 + 相机 + 麦克风 + 相机和麦克风 + 打开设置 + 授予权限 + 网络连接 + WiFi + 有线以太网 + 移动网络 + 无网络连接 + 其他 + 已转发 + 不允许SimpleX 链接 + 不允许文件和媒体 + 不允许语音消息 + SimpleX 链接 + 允许发送 SimpleX 链接。 + 群成员可发送 SimpleX 链接。 + 禁止发送 SimpleX 链接 + 此群禁止 SimpleX 链接。 + 所有者 + 启用对象 + 管理员 + 所有成员 + 下载 + 转发消息… + 转发 + 收件人看不到这条消息来自何人。 + 已保存 + 已保存 + 保存自 + 保存自%s + 已转发 + 转发自 + 蓝牙 + 并行接收 + 耳机 + 头戴式耳机 + 扬声器 + 通话声音 + 消息来源保持私密。 + 更可靠的网络连接。 + 网络管理 + 转发并保存消息 + 立陶宛语用户界面 + 连接音频和视频通话时。 + 将在私聊中启用! + 个人资料图 + 改变个人资料图形状 + 方形、圆形、或两者之间的任意形状 \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js index 5ea2f062b0..571c494c7c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js @@ -36,9 +36,10 @@ var localizedState = ""; var localizedDescription = ""; const processCommand = (function () { const defaultIceServers = [ + { urls: ["stuns:stun.simplex.im:443"] }, { urls: ["stun:stun.simplex.im:443"] }, - { urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, - { urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, + //{urls: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"}, + { urls: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj" }, ]; function getCallConfig(encodedInsertableStreams, iceServers, relay) { return { @@ -110,7 +111,17 @@ const processCommand = (function () { }); } async function initializeCall(config, mediaType, aesKey) { - const pc = new RTCPeerConnection(config.peerConnectionConfig); + var _a; + let pc; + try { + pc = new RTCPeerConnection(config.peerConnectionConfig); + } + catch (e) { + console.log("Error while constructing RTCPeerConnection, will try without 'stuns' specified: " + e); + const withoutStuns = (_a = config.peerConnectionConfig.iceServers) === null || _a === void 0 ? void 0 : _a.filter((elem) => typeof elem.urls === "string" ? !elem.urls.startsWith("stuns:") : !elem.urls.some((url) => url.startsWith("stuns:"))); + config.peerConnectionConfig.iceServers = withoutStuns; + pc = new RTCPeerConnection(config.peerConnectionConfig); + } const remoteStream = new MediaStream(); const localCamera = VideoCamera.User; const localStream = await getLocalMediaStream(mediaType, localCamera); @@ -375,6 +386,9 @@ const processCommand = (function () { // setupVideoElement(videos.remote) videos.local.srcObject = call.localStream; videos.remote.srcObject = call.remoteStream; + // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture + videos.local.play(); + videos.remote.play(); } async function setupEncryptionWorker(call) { if (call.aesKey) { @@ -439,7 +453,11 @@ const processCommand = (function () { // For opus (where encodedFrame.type is not set) this is the TOC byte from // https://tools.ietf.org/html/rfc6716#section-3.1 var _a; - const capabilities = RTCRtpSender.getCapabilities("video"); + // Using RTCRtpReceiver instead of RTCRtpSender, see these lines: + // - if (!is_recv_codec && !is_send_codec) { + // + if (!is_recv_codec) { + // https://webrtc.googlesource.com/src.git/+/db2f52ba88cf9f98211df2dabb3f8aca9251c4a2%5E%21/ + const capabilities = RTCRtpReceiver.getCapabilities("video"); if (capabilities) { const { codecs } = capabilities; const selectedCodecIndex = codecs.findIndex((c) => c.mimeType === "video/VP8"); @@ -447,8 +465,16 @@ const processCommand = (function () { codecs.splice(selectedCodecIndex, 1); codecs.unshift(selectedCodec); for (const t of call.connection.getTransceivers()) { - if (((_a = t.sender.track) === null || _a === void 0 ? void 0 : _a.kind) === "video") { - t.setCodecPreferences(codecs); + // Firefox doesn't have this function implemented: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396922 + if (((_a = t.sender.track) === null || _a === void 0 ? void 0 : _a.kind) === "video" && t.setCodecPreferences) { + try { + t.setCodecPreferences(codecs); + } + catch (error) { + // Shouldn't be here but in case something goes wrong, it will allow to make a call with auto-selected codecs + console.log("Failed to set codec preferences, trying without any preferences: " + error); + } } } } @@ -470,8 +496,22 @@ const processCommand = (function () { } return; } - for (const t of call.localStream.getTracks()) - t.stop(); + if (!call.screenShareEnabled) { + for (const t of call.localStream.getTracks()) + t.stop(); + } + else { + // Don't stop audio track if switching to screenshare + for (const t of call.localStream.getVideoTracks()) + t.stop(); + // Replace new track from screenshare with old track from recording device + for (const t of localStream.getAudioTracks()) { + t.stop(); + localStream.removeTrack(t); + } + for (const t of call.localStream.getAudioTracks()) + localStream.addTrack(t); + } call.localCamera = camera; const audioTracks = localStream.getAudioTracks(); const videoTracks = localStream.getVideoTracks(); @@ -485,6 +525,7 @@ const processCommand = (function () { replaceTracks(pc, videoTracks); call.localStream = localStream; videos.local.srcObject = localStream; + videos.local.play(); } function replaceTracks(pc, tracks) { if (!tracks.length) @@ -530,7 +571,9 @@ const processCommand = (function () { //}, //aspectRatio: 1.33, }, - audio: true, + audio: false, + // This works with Chrome, Edge, Opera, but not with Firefox and Safari + // systemAudio: "include" }; return navigator.mediaDevices.getDisplayMedia(constraints); } diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html index 59ca2b58b0..2600062b02 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html @@ -10,7 +10,6 @@ diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index 50d143aaf5..e1dba29f04 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -60,7 +60,7 @@ actual object AudioPlayer: AudioPlayerInterface { }.onFailure { Log.e(TAG, it.stackTraceToString()) fileSource.deleteTmpFile() - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) return null } } @@ -213,7 +213,7 @@ actual object SoundPlayer: SoundPlayerInterface { override fun start(scope: CoroutineScope, sound: Boolean) { val tmpFile = File(tmpDir, UUID.randomUUID().toString()) tmpFile.deleteOnExit() - SoundPlayer::class.java.getResource("/media/ring_once.mp3").openStream()!!.use { it.copyTo(tmpFile.outputStream()) } + SoundPlayer::class.java.getResource("/media/ring_once.mp3")!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) } playing = true scope.launch { while (playing && sound) { @@ -228,3 +228,37 @@ actual object SoundPlayer: SoundPlayerInterface { AudioPlayer.stop() } } + +actual object CallSoundsPlayer: CallSoundsPlayerInterface { + private var playingJob: Job? = null + + private fun start(soundPath: String, delay: Long, scope: CoroutineScope) { + playingJob?.cancel() + val tmpFile = File(tmpDir, UUID.randomUUID().toString()) + tmpFile.deleteOnExit() + SoundPlayer::class.java.getResource(soundPath)!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) } + playingJob = scope.launch { + while (isActive) { + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true) + delay(delay) + } + } + } + + override fun startConnectingCallSound(scope: CoroutineScope) { + // Taken from https://github.com/TelegramOrg/Telegram-Android + // https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE + start("/media/connecting_call.mp3", 3000, scope) + } + + override fun startInCallSound(scope: CoroutineScope) { + start("/media/in_call.mp3", 5000, scope) + } + + override fun vibrate(times: Int) {} + + override fun stop() { + playingJob?.cancel() + AudioPlayer.stop() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt index d1442bc5e3..a10f675085 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt @@ -9,6 +9,7 @@ import java.io.File import java.net.URI import java.net.URLEncoder import chat.simplex.res.MR +import java.awt.Desktop actual fun UriHandler.sendEmail(subject: String, body: CharSequence) { val subjectEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20") @@ -32,6 +33,7 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { showToast(generalGetString(MR.strings.file_saved)) } catch (e: Exception) { Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) } } else { copyFileToFile(File(absolutePath), to) {} @@ -40,3 +42,30 @@ actual fun shareFile(text: String, fileSource: CryptoFile) { }.launch(fileSource.filePath) } } + +actual fun openFile(fileSource: CryptoFile) { + try { + val filePath = filePathForShare(fileSource) ?: return + Desktop.getDesktop().open(File(filePath)) + } catch (e: Exception) { + Log.e(TAG, "Unable to open the file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + } +} + +fun filePathForShare(fileSource: CryptoFile): String? { + return if (fileSource.cryptoArgs != null) { + val tmpFile = File(tmpDir, fileSource.filePath) + tmpFile.deleteOnExit() + try { + decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return null, tmpFile.absolutePath) + } catch (e: Exception) { + Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString()) + return null + } + tmpFile.absolutePath + } else { + getAppFilePath(fileSource.filePath) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt index b5a0e2e008..e49dc6f2b1 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt @@ -16,7 +16,7 @@ actual fun LocalMultiplatformView(): Any? = null @Composable actual fun getKeyboardState(): State = remember { mutableStateOf(KeyboardState.Opened) } -actual fun hideKeyboard(view: Any?) {} +actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {} actual fun androidIsFinishingMainActivity(): Boolean = false diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt index dcae784662..50eeaee604 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt @@ -70,7 +70,7 @@ actual class VideoPlayer actual constructor( } }.onFailure { Log.e(TAG, it.stackTraceToString()) - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.stackTraceToString()) brokenVideo.value = true return false } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index 87c2d3e8f2..d3bf1bf01e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -1,30 +1,15 @@ package chat.simplex.common.views.call -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import org.nanohttpd.protocols.http.IHTTPSession import org.nanohttpd.protocols.http.response.Response @@ -45,6 +30,7 @@ actual fun ActiveCallView() { if (call != null) withBGApi { chatModel.callManager.endCall(call) } } BackHandler(onBack = endCall) + val scope = rememberCoroutineScope() WebRTCController(chatModel.callCommand) { apiMsg -> Log.d(TAG, "received from WebRTCController: $apiMsg") val call = chatModel.activeCall.value @@ -56,6 +42,8 @@ actual fun ActiveCallView() { val callType = CallType(call.localMedia, r.capabilities) chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType) chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) + CallSoundsPlayer.startConnectingCallSound(scope) + activeCallWaitDeliveryReceipt(scope) } is WCallResponse.Offer -> withBGApi { chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities) @@ -64,6 +52,7 @@ actual fun ActiveCallView() { is WCallResponse.Answer -> withBGApi { chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates) chatModel.activeCall.value = call.copy(callState = CallState.Negotiated) + CallSoundsPlayer.stop() } is WCallResponse.Ice -> withBGApi { chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates) @@ -121,6 +110,7 @@ actual fun ActiveCallView() { // After the first call, End command gets added to the list which prevents making another calls chatModel.callCommand.removeAll { it is WCallCommand.End } onDispose { + CallSoundsPlayer.stop() chatModel.activeCallViewIsVisible.value = false chatModel.callCommand.clear() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt index 30e1ad0352..cd206c8e4e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt @@ -59,19 +59,7 @@ actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = w } if (fileSource != null) { - val filePath: String = if (fileSource.cryptoArgs != null) { - val tmpFile = File(tmpDir, fileSource.filePath) - tmpFile.deleteOnExit() - try { - decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return@withLongRunningApi, tmpFile.absolutePath) - } catch (e: Exception) { - Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString()) - return@withLongRunningApi - } - tmpFile.absolutePath - } else { - getAppFilePath(fileSource.filePath) - } + val filePath = filePathForShare(fileSource) ?: return@withLongRunningApi when { desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\"")) else -> clipboard.setText(AnnotatedString(filePath)) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index 9fa93cdfdf..641ddb8744 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -15,7 +15,6 @@ import chat.simplex.common.simplexWindowState import kotlinx.coroutines.delay import java.io.ByteArrayInputStream import java.io.File -import java.io.* import java.net.URI import javax.imageio.ImageIO import kotlin.io.encoding.Base64 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index 5ffc68be79..4e4846bc9f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -62,6 +62,9 @@ fun AppearanceScope.AppearanceLayout( } } } + SectionDividerSpaced(maxTopPadding = true) + ProfileImageSection() + SectionDividerSpaced(maxTopPadding = true) ThemesSection(systemDarkTheme, showSettingsModal, editColor) SectionBottomSpacer() diff --git a/apps/multiplatform/common/src/desktopMain/resources/media/connecting_call.mp3 b/apps/multiplatform/common/src/desktopMain/resources/media/connecting_call.mp3 new file mode 100644 index 0000000000..fc425bab97 Binary files /dev/null and b/apps/multiplatform/common/src/desktopMain/resources/media/connecting_call.mp3 differ diff --git a/apps/multiplatform/common/src/desktopMain/resources/media/in_call.mp3 b/apps/multiplatform/common/src/desktopMain/resources/media/in_call.mp3 new file mode 100644 index 0000000000..1049be4462 Binary files /dev/null and b/apps/multiplatform/common/src/desktopMain/resources/media/in_call.mp3 differ diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 9918dadee1..fcbf8b9d9e 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -26,11 +26,11 @@ android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=5.6.1 -android.version_code=193 +android.version_name=5.7.3 +android.version_code=206 -desktop.version_name=5.6.1 -desktop.version_code=36 +desktop.version_name=5.7.3 +desktop.version_code=44 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 1d7a866051..76f57585a8 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -31,6 +31,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util ((<$?>)) import Data.Char (isSpace) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index c1428881b9..d158b57e22 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -36,6 +36,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatResponse, simplexChatContact) import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) diff --git a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md index 219dd2d5fe..1e4c3adfb5 100644 --- a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md +++ b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md @@ -17,12 +17,12 @@ This post also covers various aspects of end-to-end encryption, compares differe - [Why do we need end-to-end encryption?](#why-do-we-need-end-to-end-encryption) - [Why is encryption even allowed?](#why-is-encryption-even-allowed) - [End-to-end encryption security: attacks and defense.](#end-to-end-encryption-security-attacks-and-defense) - - Compromised message size - mitigated by padding messages to a fixed block size. - - Compromised confidentiality - mitigated by repudiation (deniability). - - Compromised message keys - mitigated by forward secrecy. - - Compromised long-term or session - mitigated by break-in recovery. - - Man-in-the-middle attack - mitigated by two-factor key exchange. - - "Record now, decrypt later" attacks - mitigated by post-quantum cryptography. + 1. Compromised message size - mitigated by padding messages to a fixed block size. + 2. Compromised confidentiality - mitigated by repudiation (deniability). + 3. Compromised message keys - mitigated by forward secrecy. + 4. Compromised long-term or session - mitigated by break-in recovery. + 5. Man-in-the-middle attack - mitigated by two-factor key exchange. + 6. "Record now, decrypt later" attacks - mitigated by post-quantum cryptography. - [How secure is encryption in different messengers?](#how-secure-is-end-to-end-encryption-in-different-messengers) - [Adding quantum resistance to Signal double ratchet algorithm.](#adding-quantum-resistance-to-signal-double-ratchet-algorithm) - [When can you start using quantum resistant chats?](#when-can-you-start-using-quantum-resistant-chats) diff --git a/blog/20240416-dangers-of-metadata-in-messengers.md b/blog/20240416-dangers-of-metadata-in-messengers.md new file mode 100644 index 0000000000..3b30003798 --- /dev/null +++ b/blog/20240416-dangers-of-metadata-in-messengers.md @@ -0,0 +1,52 @@ +--- +layout: layouts/article.html +title: "The dangers of metadata in messengers" +date: 2024-04-16 +previewBody: blog_previews/20240416.html +image: images/20240416-metadata.png +imageWide: true +permalink: "/blog/20240416-dangers-of-metadata-in-messengers.html" +--- + +# The dangers of metadata in messengers + +**Published:** Apr 16, 2024 + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +In many countries around the world, phone numbers are attached to biometrics data and personal IDs. Telecommunications companies are either government owned or are heavily regulated, privately owned monopolies who comply with most government requests for backdoors or user data. The idea that today, we still need to give out our phone numbers as primary identifiers to be able to use the leading messaging apps should be frowned upon and actively challenged. It’s necessary to advocate for private alternatives in messaging that do not rely on user IDs of any kind - and yes, it’s possible. + +Messaging is still not where it needs to be. Privacy is confused with security, when both are not synonymous, and there are major gaps in helping users understand the fundamental differences. + + + +For example, while WhatsApp messages are [end-to-end encrypted](https://faq.whatsapp.com/820124435853543), let’s consider what you give up when you use it, per its own listings in app stores: + +- App activity (app interactions, in-app search history, and other user-generated content) +- Location +- Financial information (user payment info and payment history) +- Contacts and their phone numbers +- Groups you’re a member of +- When you use the app and how often you use it +- Device and other IDs +- Personal info (email address, user IDs, phone number) + +This is called [metadata](https://en.wikipedia.org/wiki/Metadata). It reveals a wealth of information about you and your connections, and in the hands of a centralized monopoly, this can and does get misused in incredibly dangerous ways. Once such metadata is logged, it can create very detailed profiles about who you are, everywhere you’ve been, and everyone you’ve ever spoken to. In settling for apps that normalize this while giving you the illusion of privacy in their marketing, we are doing ourselves a disservice by accepting this as the default. Collectively, we aren’t doing enough to protect ourselves and our social graph from this invasive overreach. + +When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But it’s important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](https://simplex.chat/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.html#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so it’s something that shouldn’t necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).) + + + +This is one of the many reasons why we need to resist giving out our phone numbers just to access an app, especially to do something as personal and intimate as private messaging. Even though users can sometimes mask their numbers with a username, their identity on the app is still fundamentally tied to their phone number. App operators have access to this, as well as user contacts. Additionally, with a simple modification to the app's source code, the contacts may also gain access in some cases. This should raise more concerns about privacy, and it makes the need for anonymity difficult to achieve. + +Everyone has a different threat model (and if you don’t yet, now is a good time to [create one](https://www.privacyguides.org/en/basics/threat-modeling/#creating-your-threat-model)). For many users today, WhatsApp and other apps may be sufficient for their specific needs, especially in connecting with families and friends who are already on the app and unlikely to migrate elsewhere. If that suits your life and needs, and if you’re aware and consciously accept the risks, great. + +But we also need to acknowledge that the world is becoming increasingly dangerous in the way AI is being used to [supercharge surveillance](https://www.forbes.com/sites/forbestechcouncil/2024/02/02/artificial-intelligence-the-new-eyes-of-surveillance/?sh=cd57bc214f27), and we need to be educated and aware of the risks this is already having on our lives and what it subjects others in your network to when you choose metadata-heavy apps as your primary form of communication. Having alternatives will always be important, even if it’s not what you default to for everyday messaging. Recognize who in your social circles might require the extra privacy, anonymity and security, so that you can play a role in protecting vulnerable individuals who need it most. The messaging app you choose implicates others as well, not just yourself, and while you personally may not require complete privacy, others might have their lives depend on it. + +End-to-end encryption is a solid start, but it's just the beginning of our pursuit for true privacy and security. True privacy means that even when legal demands come knocking, there's no useful metadata to hand over. It's not enough to just protect the content of messages; we need consistent innovation in protecting metadata too. + +Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](https://simplex.chat/docs/server.html) or [XFTP server](https://simplex.chat/docs/xftp-server.html), access via Tor, [chat profiles](https://simplex.chat/docs/guide/chat-profiles.html) with incognito mode, etc.) + +As of today, most messaging alternatives, including SimpleX, will have some limitations. But with the limited resources we have, we are committed to daily progress towards creating a truly private messenger that anyone can use while maintaining the features that users have come to know and love in messaging interfaces. We want to be the prime example of a messenger that achieves genuine privacy without compromising it for convenience. We need to be able to reliably move away from small and niche use cases to endorsing and enforcing global standards for privacy and making it accessible for all users regardless of their technical expertise. + +We’re grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html), because we don’t want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word. diff --git a/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md new file mode 100644 index 0000000000..a321dbc6fa --- /dev/null +++ b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md @@ -0,0 +1,115 @@ +--- +layout: layouts/article.html +title: "SimpleX network: legally binding transparency, v5.7 released with better calls and messages" +date: 2024-04-26 +previewBody: blog_previews/20240426.html +image: images/20240426-profile.png +imageBottom: true +permalink: "/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html" +--- + +# SimpleX network: legally binding transparency, v5.7 released with better calls and messages + +What's new in v5.7: +- [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption) with all contacts. +- [forward and save messages](#forward-and-save-messages) without revealing the source. +- [in-call sounds and switching sound sources](#in-call-sounds-and-switching-sound-sources). +- [better network connection management](#network-management). +- [customizable profile images](#customizable-shape-of-profile-images) + +Also, we added Lithuanian interface language to the Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +## Legally binding transparency + +We are committed to open-source, privacy and security. Here are the recent changes we made: + +- We now have a [Transparency Reports](https://simplex.chat/transparency/) page. +- We updated our [Privacy Policy](https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files). +- We published a new page with [Frequently Asked Questions](https://simplex.chat/faq/), thanks to the guidance from users. +- We also have a new [Security Policy](https://simplex.chat/security/) – we welcome your feedback on it. + +What do we mean by “legally binding transparency?”. It includes these principles: +- Accountability: an empty promise or commitment to transparency that is not legally binding is just marketing, and can provide opportunities for the organizations to be misleading or not disclose important information that can affect their users privacy and security. +- Consistency: often, there's a disconnect between marketing claims and legally binding policies. Our approach is to ensure that promises made in marketing materials or any external communications align with our legally binding documents, so that users can rely on our promises, and know exactly what to expect from us. + +For example: +- we use open-source code, and we made a legally binding commitment to use the published code in all released apps and deployed preset servers. +- we use precise and technical language in the Privacy Policy defining what data and metadata can be accessed via the preset relays. + +If you see any inconsistency between technical parameters of SimpleX Network and what is promised in our Privacy Policy please raise it with us. + +## What's new in v5.7 + +This release focus is improving the app usability, and preparing the foundation for v5.8 that will provide an in-built protection of user IP addresses when connecting to unknown file and messaging servers, reducing the need to use Tor (which would still remain supported via SOCKS proxy, for additional privacy). + +### Quantum resistant end-to-end encryption + + + +We [wrote before](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) about how quantum resistant encryption was added to SimpleX Chat and also about other properties of end-to-end encryption, possible attacks on its security and known mitigations. + +Quantum resistant encryption will now be enabled by default in all direct chats. For the new conversations it will be enabled from the beginning, and for the existing conversations it will be agreed after you exchange several messages with your contacts - you will see a notice in the conversation when it happens. + +You can still safely downgrade the app to an earlier version if needed, as v5.6 already supports quantum resistant encryption. + +With the users who have an earlier version, the app will work using the conventional encryption, which is still very secure. It's important that we augmented the conventional encryption with post-quantum cryptographic algorithm rather than replaced it, using a hybrid construction as recommended by the cryptography experts. + +The groups currently do not support quantum resistant encryption yet - we plan to add it in the future to small groups. + +### Forward and save messages + +You can now save received messages to private notes and forward them to your contacts and groups. This is both more convenient than copy-pasting the messages, and also more private - you can forward files and preserve a disappearing file in your private notes without saving them outside of the app. + +You can see and navigate to the original source of the message via the message information, but the recipient of the message can only see that it was forwarded, but not from which conversation - in this way you can show that the message was quoted from another source without revealing the source ([Chatham House Rule](https://en.wikipedia.org/wiki/Chatham_House_rule)). + + + +### In-call sounds and switching sound sources + +This was the most frequent request of the users who use SimpleX Chat for audio and video calls - to add sound indication to the connection progress, as happens in all other apps. This release added these sounds and also vibration when call connects and disconnects. + +You can also switch between bluetooth headphones, speakerphone and earpiece via the new button in the call. + +### Network management + +To reduce traffic and battery usage, this release made connection timeouts dependent on which network your device uses - timeouts will be larger when you are on mobile connection and smaller when on WiFi. It also makes connection attempts very infrequent when the device is not connected to network. + +### Customizable shape of profile images + +To customizable the interface, you can now change the shapes of profile images to anything from squares with sharp corners to circles, as before, via the Appearance menu in the app Settings. The default shape of profile images is changed to squares with rounded corners. + + + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donates to SimpleX Chat! + +We are planning a 3rd party security audit for the protocols and cryptography design in July 2024, and also the security audit for an implementation in December 2024/January 2025, and it would hugely help us if some part of this $50,000+ expense is covered with donations. + +We are prioritizing users privacy and security - it would be impossible without your support. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX network based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/README.md b/blog/README.md index cf93260676..2f3b5aeeb2 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,28 @@ # Blog +Apr 26, 2024 [SimpleX network: legally binding transparency, v5.7 released with better calls and messages](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) + +We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy. + +We are committed to have full consistency between marketing promises and legally binding documents. + +What's new in v5.7: +- quantum resistant end-to-end encryption with all contacts. +- forward and save messages without revealing the source. +- in-call sounds and switching sound sources. +- better network connection management. +- customizable profile images + +--- + +Apr 16. 2024 [The dangers of metadata in messengers](./20240416-dangers-of-metadata-in-messengers.md) + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +It's important not to be complacent with the current standards of messaging, where metadata aggregation is still normalized in apps falsely and dangerously marketed as "private". This is a post exploring the fundamental differences between privacy and security. + +--- + Apr 4. 2024 [Why I joined SimpleX Chat](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md) _By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ diff --git a/blog/images/20240416-metadata.png b/blog/images/20240416-metadata.png new file mode 100644 index 0000000000..743930bf15 Binary files /dev/null and b/blog/images/20240416-metadata.png differ diff --git a/blog/images/20240416-whatsapp.jpg b/blog/images/20240416-whatsapp.jpg new file mode 100644 index 0000000000..399235347a Binary files /dev/null and b/blog/images/20240416-whatsapp.jpg differ diff --git a/blog/images/20240426-forward1.png b/blog/images/20240426-forward1.png new file mode 100644 index 0000000000..8fb99a4580 Binary files /dev/null and b/blog/images/20240426-forward1.png differ diff --git a/blog/images/20240426-forward2.png b/blog/images/20240426-forward2.png new file mode 100644 index 0000000000..8dd5043703 Binary files /dev/null and b/blog/images/20240426-forward2.png differ diff --git a/blog/images/20240426-forward3.png b/blog/images/20240426-forward3.png new file mode 100644 index 0000000000..9cf97e8ae1 Binary files /dev/null and b/blog/images/20240426-forward3.png differ diff --git a/blog/images/20240426-pq.png b/blog/images/20240426-pq.png new file mode 100644 index 0000000000..2a86d10a7c Binary files /dev/null and b/blog/images/20240426-pq.png differ diff --git a/blog/images/20240426-profile.png b/blog/images/20240426-profile.png new file mode 100644 index 0000000000..fc0b9536bb Binary files /dev/null and b/blog/images/20240426-profile.png differ diff --git a/blog/images/20240426-profile1.png b/blog/images/20240426-profile1.png new file mode 100644 index 0000000000..f638b3b7bc Binary files /dev/null and b/blog/images/20240426-profile1.png differ diff --git a/blog/images/20240426-profile2.png b/blog/images/20240426-profile2.png new file mode 100644 index 0000000000..6592eec886 Binary files /dev/null and b/blog/images/20240426-profile2.png differ diff --git a/blog/images/20240426-profile3.png b/blog/images/20240426-profile3.png new file mode 100644 index 0000000000..f6972f2013 Binary files /dev/null and b/blog/images/20240426-profile3.png differ diff --git a/cabal.project b/cabal.project index 62115c136b..c95e3c8f0c 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: 6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd + tag: 769e54db76fc33a04085410cdf78ae77ed9717b6 source-repository-package type: git diff --git a/docs/ANDROID.md b/docs/ANDROID.md index fa8921c827..61f81d1a40 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -3,7 +3,7 @@ title: Accessing files in Android app revision: 07.02.2023 --- -| 07.02.2023 | EN, [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md) | +| 07.02.2023 | EN, [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md), [PL](/docs/lang/pl/ANDROID.md) | # Accessing files in Android app diff --git a/docs/CLI.md b/docs/CLI.md index baf79bb3bc..d4f799c7af 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -3,7 +3,7 @@ title: Terminal CLI revision: 31.01.2023 --- -| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md) | +| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md), [PL](/docs/lang/pl/CLI.md) | # SimpleX Chat terminal (console) app for Linux/MacOS/Windows diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index aaf452af00..bc013cd7eb 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -3,7 +3,7 @@ title: Contributing guide revision: 31.01.2023 --- -| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | +| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md), [PL](/docs/lang/pl/CONTRIBUTING.md) | # Contributing guide diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000000..a9f94b49eb --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,229 @@ +--- +title: Frequently Asked Questions +permalink: /faq/index.html +revision: 23.04.2024 +--- + +# Frequently Asked Questions + +[How to use it](#how-to-use-it) +- [I have nobody to chat with! Where can I find any groups?](#i-have-nobody-to-chat-with-where-can-i-find-any-groups) +- [What is database? What can I do with it?](#what-is-database-what-can-i-do-with-it) +- [Can I send files over SimpleX? ](#can-i-send-files-over-simplex) +- [What’s incognito profile?](#whats-incognito-profile) +- [How do invitations work?](#how-do-invitations-work) +- [How to configure and delete groups?](#how-to-configure-and-delete-groups) +- [Are there any reactions to messages? Can I answer specific messages directly?](#are-there-any-reactions-to-messages-can-i-answer-specific-messages-directly) +- [What do checkmarks mean?](#what-do-checkmarks-mean) +- [Can I use the same profile on desktop? Do messages sync cross-platform?](#can-i-use-the-same-profile-on-desktop-do-messages-sync-cross-platform) + +[Troubleshooting](#troubleshooting) +- [I do not receive messages or message notifications](#i-do-not-receive-messages-or-message-notifications) +- [I do not see the second tick on the messages I sent](#i-do-not-see-the-second-tick-on-the-messages-i-sent) +- [I see image preview but cannot open the image](#i-see-image-preview-but-cannot-open-the-image) +- [I cannot play a voice message](#i-cannot-play-a-voice-message) +- [Audio or video calls do not connect](#audio-or-video-calls-do-not-connect) +- [Audio or video calls without e2e encryption](#audio-or-video-calls-without-e2e-encryption) +- [I clicked the link to connect, but could not connect](#i-clicked-the-link-to-connect-but-could-not-connect) + +[Privacy and security](#privacy-and-security) +- [Does SimpleX support post quantum cryptography?](#does-simplex-support-post-quantum-cryptography) +- [What user data can be provided on request?](#what-user-data-can-be-provided-on-request) +- [Does SimpleX protect my IP address?](#does-simplex-protect-my-ip-address) +- [Can I host my own relays?](#can-i-host-my-own-relays) + +[Funding and business model](#funding-and-business-model) +- [How are you funded?](#how-are-you-funded) +- [Why VCs?](#why-vcs) +- [What will be the business model?](#what-will-be-the-business-model) + +## How to use it + +### I have nobody to chat with! Where can I find any groups? + +Please check our [Groups Directory](./DIRECTORY.md) in the first place. You might find some interesting groups and meet even more interesting people. + +### What is database? What can I do with it? + +Database is essential for SimpleX Chat to function properly. In comparison to centralized messaging providers, it is _the user_ who is responsible for taking care of their data. On the other hand, user is sure that _nobody but them_ has access to it. Please read more about it: [Database](./guide/managing-data.md). + +### Can I send files over SimpleX? + +Of course! While doing so, you are using a _state-of-the-art_ protocol that greatly reduces metadata leaks. Please read more about it: [XFTP Protocol](../blog/20230301-simplex-file-transfer-protocol.md). + +### What’s incognito profile? + +This feature is unique to SimpleX Chat – it is independent from chat profiles. + +When "Incognito Mode” is turned on, your currently chosen profile name and image are hidden from your new contacts. It allows anonymous connections with other people without any shared data – when you make new connections or join groups via a link a new random profile name will be generated for each connection. + +### How do invitations work? + +It is quite a complex process, but fortunately all of this happens in the background, so it's simply to use. + +Whenever somebody connects to you via your address, they basically ask your client whether they want to establish connection. After that, you can either agree or disagree. +If interested, please read more: [Addresses and invitations](./guide/making-connections.md). + +### How to configure and delete groups? + +Please check: [Users guide](./guide/secret-groups.md). + +### Are there any reactions to messages? Can I answer specific messages directly? + +Yes! Currently, there are six emojis available. What's more, you can respond to specific message by holding it and selecting _Reply_. + +### What do checkmarks mean? + +It's quite simple: +- one checkmark - message is delivered to the relay (the server). +- two checkmarks - message is delivered to the recipient's device. +"sent" means accepted by the relay for delivery, "delivered" - stored on the recipient device. + +Also see [ ](#i-do-not-see-the-second-tick-on-the-messages-i-sent) + +### Can I use the same profile on desktop? Do messages sync cross-platform? + +You can use your profile from mobile device on desktop. However, to do so you need to be on the same network, both on your mobile and desktop. More about it: [Release info](../blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol). + +## Troubleshooting + +### I do not receive messages or message notifications + +There may be several reasons messages are not delivered to you from your contact: + +**You or your contact cannot connect to the server that you use to receive messages from your contact.** + +You can check which server is used to receive messages by tapping the contact name above the conversation. + +You can also run tests for this server from the app Network settings. + +Please ask your contact if they have a single tick on the message to determine if the message failed to send or if you fail to receive it. + +**Message delivery got stuck because of some unresolved bug.** + +Fully restarting the app is the workaround to resume message delivery. + +To do it on iOS, simply close the app (swipe up from the opened apps) and open it again. + +To do it on Android - choose Restart from the app settings, simply closing and re-opening the app will not restart the messaging service. + +**Your Android operating system kills the app while it is in background.** + +Check battery settings for the app - it should be set to Unrestricted. + +For some devices, there may be additional options to prevent the app from being killed - e.g., on Xiaomi you need to enable Auto Start setting for the app. Please consult https://dontkillmyapp.com site for any additional settings for your device. + +**iOS notifications failed to initialize correctly** + +Check the color of the bolt icon next to Notifications in app settings - it should be green. + +If it's not, please open notifications, disable them (choose Off / Local), and then enable again - you should do it when you have Internet connection. + +If the above didn't help, the reason could be that iOS failed to issue notification token - we have seen this issue several times. In this case, restarting the whole device should help. + +**Messaging server or notification server is under maintenance** + +Please check the current status of preset servers at [https://status.simplex.chat](https://status.simplex.chat). You can also connect to status bot via QR code on that page - it will send the updates when the server is offline for maintenance, and also when the new versions of the app are released. + +### I do not see the second tick on the messages I sent + +You may not have the second tick on your sent messages for these reasons: + +- your contact is not online, and did not receive your message. +- possibly, message delivery to your contact or to you is disrupted - see [I do not receive messages](#i-do-not-receive-messages-or-message-notifications) - please check with your contact via some other channel if they received your message. If the message was delivered, then it means your device could fail to receive the delivery notification. +- possibly, your contact disabled sending delivery receipts - it can be disabled for specific or for all contacts - please check with your contact. + +### I see image preview but cannot open the image + +It can be for these reasons: +- your contact did not finish uploading the image file, possibly closing the app too quickly. When the image file is fully uploaded there will be a tick in the _top right corner_ or the image +- your device fails to receive it. Please check server connectivity and run server tests, and also try increasing network timeouts in Advanced network settings. File reception was substantially improved in v5.7 - please make sure you are using the latest version. +- file expired and can no longer be received. Files can be received only for 2 days after they were sent, after that they won't be available and will show X in the top right corner. + +### I cannot play a voice message + +This can happen for similar reasons as for [images](#i-see-image-preview-but-cannot-open-the-image). + +Please check your network settings and make sure you use the latest version of the app. + +Please report such issues if you use v5.7 or newer. + +### Audio or video calls do not connect + +App uses WebRTC for calls. Please check that you can connect to the servers configured in the app: stun.simplex.im and turn.simplex.im - see [troubleshooting WebRTC](./WEBRTC.md#troubleshoot). + +If you can connect to the server, please report this issue to us privately, including the following information: + +- how you connect to the network: WiFi, mobile network, VPN provider - the more information you can provide the better. + +- app version and platform. For mobile apps, it would help if you can make a screen recording from both devices during unsuccessful calls and share with us. + +- if the issue is on desktop app, which browser is used for calls. In this case also please check browser console during the call and send us the log, ideally from both sides of the unsuccessful calls. + +Thank you for helping us debug and improve calls. + +### Audio or video calls without e2e encryption + +During the call, the app indicates whether or not the call has end-to-end encryption. + +If one of the call parties uses Android (or desktop) app, the call would use Android system webview (or browser). Some older systems do not support media stream encryption, in which case the call will connect without it. + +To determine whether it is the limitation of your, your contact's or both devices: +- if some of your calls have e2e encryption but some don't, then it's certainly the old webview version or browser of your contacts - please ask them to upgrade. +- if you are not sure, you can check at what point "no e2e encryption" appears: + - if it is shown when the call rings on your device, then your contact's device does not support call encryption. + - if it is shown on your screen as soon as you start the call, then your device does not support call encryption. + - if in the beginning of the call your device shows "e2e encryption" but when your contact accepts the call it changes to "no e2e encryption", then it is only your contact's device that does not support it. + +You need to upgrade webview (some Android systems allow it), Android system or the device to have support for e2e encryption in the calls - all modern webviews (and browsers) support it. + +### I clicked the link to connect, but could not connect + +If you confirmed the connection in the app, pending connection will be shown in the list of chats - you can assign the name to it, so you know who it was when your contact is connected (e.g., if they choose some name you don't recognize). + +For connection to complete, your contact has to be online and have the app running - please ask them to open the app, and try to have the app open at the same time - it will help to complete the connection faster. + +Once the connection is established you don't need to be online at the same time to send messages. + +## Privacy and security + +### Does SimpleX support post quantum cryptography? + +Yes! Please read more about quantum resistant encryption is added to SimpleX Chat and about various properties of end-to-end encryption in [this post](../blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md). + +### What user data can be provided on request? + +Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances. + +Please see our [Privacy Policy](../PRIVACY.md) and [Transparency Reports](./TRANSPARENCY.md). + +### Does SimpleX protect my IP address? + +Not fully yet, it is a work in progress. While your device does not connect to your contacts' devices directly, as it happens in p2p networks, your contacts can self-host their relays, and you will connect to them when sending messages. A modified relay can record IP addresses connecting devices, as is the case with any other server, including Tor entry nodes, VPN providers, etc. - IP address is fundamental to Internet functioning, and there will always be some server that can observe your IP address. + +We are currently working on the next version of message routing protocol that will protect your IP address from the relays chosen by your contacts, so it will only be visible to the relays chosen by you. Read about technical details here: [RFC](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2023-09-12-second-relays.md). + +### Can I host my own relays? + +Of course! Please check these tutorials: [SMP server](./SERVER.md) and [XFTP server](./XFTP-SERVER.md). + +## Funding and business model + +### How are you funded? + +SimpleX Chat Ltd is funded by private investors and venture capital. As an open-source project, it is also being generously supported by donations as well. Read [more details](../blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md#how-is-it-funded-and-what-is-the-business-model). + +### Why VCs? + +Here are some reflections on VC funding being a necessity for a project at this scale, as well as sustainability and profitability for longtime operations: https://www.poberezkin.com/posts/2023-10-31-why-privacy-impossible-without-venture-funding.html + +And another perspective from a team member on the delicate balance of venture-backed and nonprofit structures, and the plans for the SimpleX network protocols to evolve under the stewardship of nonprofit entities in various jurisdictions, so that its continued evolution aligns more closely with the vision of community-driven, independent and transparent governance: +[https://simplex.chat/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.html](../blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md). + +### What will be the business model? + +We are focusing on product-market fit, and as such the business model is still a work in progress. However, the app will have a freemium model with extra features or capabilities for paid users (taking into consideration a potential formula like 5% paying $5/month is $3/user/year - ~90% gross profit margin). + +The other income stream would be via business services, for entities needing direct and customized support to integrate with the SimpleX protocol or related resources. There will also be a revenue-sharing model from customers to network operators, to provide an incentive for them to continue running nodes, which will increase decentralization and reliability of the network. + +Non-exploitative commercial models with fully open source code are not easy to achieve, and we’re committed to finding the best possible fit for our context. Everything will be fully communicated as this plan progresses. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000000..77f588ec26 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,79 @@ +--- +title: Security Policy +permalink: /security/index.html +revision: 23.04.2024 +--- + +# Security Policy + +While great care is taken to ensure the highest level of security and privacy in SimpleX network servers and clients, all software can have flaws, and we believe it is a critical part of an organization's social responsibility to minimize the impact of these flaws through continual vulnerability discovery efforts, defense in depth design, and prompt remediation and notification. + +The security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html). + +We are planning design review of SimpleX protocols in July 2024 and implementation review in December 2024/January 2025. + +## Reporting security issues + +To report a security issue, please contact us directly via email [chat@simplex.chat](mailto:chat@simplex.chat). Please do NOT report security issues via GitHub issues or via any public channels. + +Please encrypt the email message using the key for this address from [keys.openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) (its fingerprint is `FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC`) and make your key available for a secure reply. + +While we encourage you to encrypt the message, if this poses a barrier to reporting, an unencrypted submission is better than no submission. + +## Issue triage + +Our team will investigate and prioritize the reported issue. We may work in private with individuals who are not our direct team members, as well as other organizations, where we believe this can help with the issue investigation, resolution, or testing. + +## Threat Model + +Please review threat model for SimpleX: https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model + +Certain threats are considered out of the scope of SimpleX security model. For example, we do not consider SimpleX secure against the following classes of attacks: + +- CPU/hardware flaws. +- physical observation side channels (e.g. power consumption, EM emissions, etc). + +Further, any user data stored on the device can be accessed with user's or root privileges, such as: +- user files in the app storage (encrypted or not, depending on the app settings). +- encrypted database. +- database encryption key in case it is stored on the device. + +Mitigations for security issues outside of our threat model may still be implemented, however they will be weighed against competing priorities, and we do not classify them as SimpleX vulnerabilities. + +## Issue severity + +We will determine the risk of each issue, taking into account our experience dealing with past issues, versions affected, common defaults, and use cases. We classify issues on two dimensions, based on their severity level and the difficulty required to exploit them. + +**Difficulty levels** + +- **Low**: The flaw is well known; public tools for its exploitation exist or can be scripted. +- **Medium**: An attacker must write an exploit or will need in-depth knowledge of the system. +- **High**: An attacker must have privileged access to the system, may need to know complex technical details, or must discover other weaknesses to exploit this issue. + +**Issue severity levels** + +- **CRITICAL severity**. Such issues should affect common configurations and be exploitable with low or medium difficulty. For example: significant disclosure of the encrypted users messages or files either via relays or via communication channels, vulnerabilities which can be easily exploited remotely to compromise clients or servers private keys. These issues will be kept private and will trigger a new release of all supported versions. +- **HIGH severity**. This includes issues that are of a lower risk than critical, possibly due to affecting less common configurations, or have high difficulty to be exploited. These issues will be kept private and will trigger a new release of all supported versions. +- **MEDIUM severity**. This includes issues like crashes in client applications caused by the received messages or files, flaws in protocols that are less commonly used, and local flaws. These will in general be kept private until the next release, and that release will be scheduled so that it can roll up several such flaws at one time. +- **LOW severity**. This includes issues such as those that only affect the SimpleX CLI app, or unlikely configurations, or issues that would be classified as medium but are very difficult to exploit. These will in general be fixed immediately in latest development versions, and may be back-ported to older versions that are still getting updates. These issues may be kept private or be included in commit messages. + +## Notification policy + +Security fixes of critical, high and medium severity MUST NOT be mentioned in the commit message. Security fixes of low severity MAY be mentioned in the commit messages. + +We will privately notify trusted partners about forthcoming security fixes on the day when the fix is publicly released and available for download via all supported channels, indicating the issue level, but not further details. + +7 days after the new software version is released and available for download via all supported channels, we will indicate that it fixes a security issue and its level, but not further details. This notification will be published in our release notes and broadcast channels we use. + +14 days later the details will be published in the release notes, describing the impact and the nature of vulnerability, but not necessarily providing detailed instruction for the exploit - it will be decided on a case by case basis. + +## Trusted partners + +You may be privately notified about the forthcoming releases containing fixes to the security issues of critical, high and medium severity. We will communicate only the level of the issue severity, and not the issue itself. + +To be included in this list, you should be one of the following: +- software or hardware vendor depending on our code. +- commercial or non-profit organization that uses our software in scenarios where security and privacy is critically important. +- we may also include other organizations that are not listed but would otherwise qualify for list membership. +- we may also include organizations with which we have a commercial relationship. +- we may withdraw notifying certain organizations if they leak issues before they are public or do not add value. diff --git a/docs/SERVER.md b/docs/SERVER.md index e476c7250c..c29a805452 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -3,7 +3,7 @@ title: Hosting your own SMP Server revision: 31.07.2023 --- -| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | +| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | # Hosting your own SMP Server diff --git a/docs/SIMPLEX.md b/docs/SIMPLEX.md index 7ed01efa3c..ec25afaf88 100644 --- a/docs/SIMPLEX.md +++ b/docs/SIMPLEX.md @@ -3,7 +3,7 @@ title: SimpleX platform revision: 07.02.2023 --- -| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | +| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md), [PL](/docs/lang/pl/SIMPLEX.md) | # SimpleX platform - motivation and comparison ## Problems diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md index 85b24b368e..d5c1cdef0b 100644 --- a/docs/TRANSLATIONS.md +++ b/docs/TRANSLATIONS.md @@ -3,7 +3,7 @@ title: Contributing translations to SimpleX Chat revision: 19.03.2023 --- -| 19.03.2023 | EN, [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md) | +| 19.03.2023 | EN, [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md), [PL](/docs/lang/pl/TRANSLATIONS.md) | # Contributing translations to SimpleX Chat diff --git a/docs/TRANSPARENCY.md b/docs/TRANSPARENCY.md new file mode 100644 index 0000000000..547660861b --- /dev/null +++ b/docs/TRANSPARENCY.md @@ -0,0 +1,29 @@ +--- +title: Transparency Reports +permalink: /transparency/index.html +revision: 26.04.2024 +--- + +# Transparency Reports + +**Updated**: Apr 26, 2024 + +SimpleX Chat Ltd. is a company registered in the UK – it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators. + +This page will include any and all reports on requests for user data. + +*To date, we received none*. + +Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances. + +**Helpful resources**: +- [Privacy policy](../PRIVACY.md) +- [Privacy and security: technical details and limitations](../README.md#privacy-and-security-technical-details-and-limitations) +- Whitepaper: + - [Trust in servers](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#trust-in-servers) + - [Encryption Primitives Used](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#encryption-primitives-used) + - [Threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model) + +Have a more specific question? Reach out to us via [SimpleX Chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) or via email [chat@simplex.chat](mailto:chat@simplex.chat). + +For any sensitive questions please use SimpleX Chat or encrypted email messages using the key for this address from [keys.openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) (its fingerprint is `FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC`) and make your key available for a secure reply. diff --git a/docs/WEBRTC.md b/docs/WEBRTC.md index 7978d21ec7..8ce31bf959 100644 --- a/docs/WEBRTC.md +++ b/docs/WEBRTC.md @@ -3,7 +3,7 @@ title: Using custom WebRTC ICE servers in SimpleX Chat revision: 31.01.2023 --- -| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | +| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md), [PL](/docs/lang/pl/WEBRTC.md) | # Using custom WebRTC ICE servers in SimpleX Chat diff --git a/docs/lang/cs/ANDROID.md b/docs/lang/cs/ANDROID.md index 4edfc3018c..3c401f1d1b 100644 --- a/docs/lang/cs/ANDROID.md +++ b/docs/lang/cs/ANDROID.md @@ -2,7 +2,7 @@ title: Přístup k souborům v aplikaci Android revision: 07.02.2023 --- -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/ANDROID.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/ANDROID.md), [PL](/docs/lang/pl/ANDROID.md) | # Přístup k souborům v aplikaci Android diff --git a/docs/lang/cs/CLI.md b/docs/lang/cs/CLI.md index aa5a2ba281..9cbce8e6fe 100644 --- a/docs/lang/cs/CLI.md +++ b/docs/lang/cs/CLI.md @@ -2,7 +2,7 @@ title: SimpleX Chat terminálová revision: 31.01.2023 --- -| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CLI.md), [FR](/docs/lang/fr/CLI.md) | +| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CLI.md), [FR](/docs/lang/fr/CLI.md), [PL](/docs/lang/pl/CLI.md) | # SimpleX Chat terminálová (konzolová) aplikace pro Linux/MacOS/Windows diff --git a/docs/lang/cs/CONTRIBUTING.md b/docs/lang/cs/CONTRIBUTING.md index 26c746e7d2..17574bed4a 100644 --- a/docs/lang/cs/CONTRIBUTING.md +++ b/docs/lang/cs/CONTRIBUTING.md @@ -2,7 +2,7 @@ title: Průvodce přispíváním revision: 31.01.2023 --- -| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CONTRIBUTING.md), [FR](/docs/lang/fr/CONTRIBUTING.md) | +| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/CONTRIBUTING.md), [FR](/docs/lang/fr/CONTRIBUTING.md), [PL](/docs/lang/pl/CONTRIBUTING.md) | # Průvodce přispíváním diff --git a/docs/lang/cs/README.md b/docs/lang/cs/README.md index 9423cc96b8..f536cb1aa6 100644 --- a/docs/lang/cs/README.md +++ b/docs/lang/cs/README.md @@ -1,4 +1,4 @@ -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/README.md), [FR](/docs/lang/fr/README.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/README.md), [FR](/docs/lang/fr/README.md), [PL](/docs/lang/pl/README.md) | SimpleX logo @@ -228,7 +228,7 @@ Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro přípa ## Připojte se ke skupině uživatelů -Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) +Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) Existuje také několik skupin v jiných jazycích než v angličtině, do kterých jsme nechali přeložit rozhraní aplikací. Tyto skupiny slouží k testování a pokládání otázek ostatním uživatelům SimpleX Chat. Ne vždy v nich odpovídáme na dotazy, proto je prosím pokládejte v některé z anglicky mluvících skupin. diff --git a/docs/lang/cs/SERVER.md b/docs/lang/cs/SERVER.md index f8258909bc..3dd2f3780c 100644 --- a/docs/lang/cs/SERVER.md +++ b/docs/lang/cs/SERVER.md @@ -2,7 +2,7 @@ title: Hostování vlastního serveru SMP revision: 05.06.2023 --- -| Aktualizováno 05.06.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md) | +| Aktualizováno 05.06.2023 | Jazyky: CZ, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | # Hostování vlastního serveru SMP diff --git a/docs/lang/cs/SIMPLEX.md b/docs/lang/cs/SIMPLEX.md index 33d45cec65..0d32fd06d9 100644 --- a/docs/lang/cs/SIMPLEX.md +++ b/docs/lang/cs/SIMPLEX.md @@ -2,7 +2,7 @@ title: Platforma SimpleX revision: 07.02.2023 --- -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/SIMPLEX.md), [FR](/docs/lang/fr/SIMPLEX.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/SIMPLEX.md), [FR](/docs/lang/fr/SIMPLEX.md), [PL](/docs/lang/pl/SIMPLEX.md) | # Platforma SimpleX - motivace a srovnání diff --git a/docs/lang/cs/TRANSLATIONS.md b/docs/lang/cs/TRANSLATIONS.md index b260bd12a6..c22979bdfc 100644 --- a/docs/lang/cs/TRANSLATIONS.md +++ b/docs/lang/cs/TRANSLATIONS.md @@ -2,7 +2,7 @@ title: Přispívání překladů do SimpleX Chat revision: 07.02.2023 --- -| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/TRANSLATIONS.md) | +| Aktualizováno 07.02.2023 | Jazyky: CZ, [EN](/docs/TRANSLATIONS.md), [PL](/docs/lang/pl/TRANSLATIONS.md) | # Přispívání překladů do SimpleX Chat diff --git a/docs/lang/cs/WEBRTC.md b/docs/lang/cs/WEBRTC.md index 77df920255..df63c3205e 100644 --- a/docs/lang/cs/WEBRTC.md +++ b/docs/lang/cs/WEBRTC.md @@ -2,7 +2,7 @@ title: Použití vlastních serverů WebRTC ICE v SimpleX Chat revision: 31.01.2023 --- -| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/WEBRTC.md), [FR](/docs/lang/fr/WEBRTC.md) | +| Aktualizováno 31.01.2023 | Jazyky: CZ, [EN](/docs/WEBRTC.md), [FR](/docs/lang/fr/WEBRTC.md), [PL](/docs/lang/pl/WEBRTC.md) | # Použití vlastních serverů WebRTC ICE v SimpleX Chat diff --git a/docs/lang/fr/ANDROID.md b/docs/lang/fr/ANDROID.md index 0f710542a8..1d100c5c04 100644 --- a/docs/lang/fr/ANDROID.md +++ b/docs/lang/fr/ANDROID.md @@ -2,7 +2,7 @@ title: Accès aux fichiers dans l'application Android revision: 07.02.2023 --- -| 07.02.2023 | FR, [EN](/docs/ANDROID.md), [CZ](/docs/lang/cs/ANDROID.md) | +| 07.02.2023 | FR, [EN](/docs/ANDROID.md), [CZ](/docs/lang/cs/ANDROID.md), [PL](/docs/lang/pl/ANDROID.md) | # Accès aux fichiers dans l'application Android diff --git a/docs/lang/fr/CLI.md b/docs/lang/fr/CLI.md index 6fe9c86d75..bb596491f1 100644 --- a/docs/lang/fr/CLI.md +++ b/docs/lang/fr/CLI.md @@ -2,7 +2,7 @@ title: Application de terminal revision: 31.01.2023 --- -| 31.01.2023 | FR, [EN](/docs/CLI.md), [CZ](/docs/lang/cs/CLI.md) | +| 31.01.2023 | FR, [EN](/docs/CLI.md), [CZ](/docs/lang/cs/CLI.md), [PL](/docs/lang/pl/CLI.md) | # Application de terminal (console) SimpleX Chat pour Linux/MacOS/Windows diff --git a/docs/lang/fr/CONTRIBUTING.md b/docs/lang/fr/CONTRIBUTING.md index 81515b09b2..ea6dcb5ca3 100644 --- a/docs/lang/fr/CONTRIBUTING.md +++ b/docs/lang/fr/CONTRIBUTING.md @@ -2,7 +2,7 @@ title: Guide pour contribuer revision: 31.01.2023 --- -| 31.01.2023 | FR, [EN](/docs/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | +| 31.01.2023 | FR, [EN](/docs/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md), [PL](/docs/lang/pl/CONTRIBUTING.md) | # Guide pour contribuer diff --git a/docs/lang/fr/README.md b/docs/lang/fr/README.md index 2f11fe9539..69401da5a2 100644 --- a/docs/lang/fr/README.md +++ b/docs/lang/fr/README.md @@ -4,7 +4,7 @@ [![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) [![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex) -| 30/03/2023 | FR, [EN](/README.md), [CZ](/docs/lang/cs/README.md) | +| 30/03/2023 | FR, [EN](/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) | SimpleX logo @@ -48,7 +48,7 @@ ## Rejoindre des groupes d'utilisateurs -Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) +Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) Il existe des groupes dans d'autres langues, dans lesquelles l'interface de l'application a été traduite. Ces groupes servent à tester et à poser des questions aux autres utilisateurs de SimpleX Chat : diff --git a/docs/lang/fr/SERVER.md b/docs/lang/fr/SERVER.md index ac9cec7eb9..7bbe315273 100644 --- a/docs/lang/fr/SERVER.md +++ b/docs/lang/fr/SERVER.md @@ -2,7 +2,7 @@ title: Héberger votre propre serveur SMP revision: 05.06.2023 --- -| 05.06.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | +| 05.06.2023 | FR, [EN](/docs/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | # Héberger votre propre serveur SMP diff --git a/docs/lang/fr/SIMPLEX.md b/docs/lang/fr/SIMPLEX.md index a7134205d0..aacc645054 100644 --- a/docs/lang/fr/SIMPLEX.md +++ b/docs/lang/fr/SIMPLEX.md @@ -2,7 +2,7 @@ title: Plateforme SimpleX revision: 07.02.2023 --- -| 07.02.2023 | FR, [EN](/docs/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | +| 07.02.2023 | FR, [EN](/docs/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md), [PL](/docs/lang/pl/SIMPLEX.md) | # Plateforme SimpleX - motivation et comparaison diff --git a/docs/lang/fr/TRANSLATIONS.md b/docs/lang/fr/TRANSLATIONS.md index e85a4a8513..1e216900e7 100644 --- a/docs/lang/fr/TRANSLATIONS.md +++ b/docs/lang/fr/TRANSLATIONS.md @@ -2,7 +2,7 @@ title: Contribuer aux traductions de SimpleX Chat revision: 19.03.2023 --- -| 19.03.2023 | FR, [EN](/docs/TRANSLATIONS.md), [CZ](/docs/lang/cs/TRANSLATIONS.md) | +| 19.03.2023 | FR, [EN](/docs/TRANSLATIONS.md), [CZ](/docs/lang/cs/TRANSLATIONS.md), [PL](/docs/lang/pl/TRANSLATIONS.md) | # Contribuer aux traductions de SimpleX Chat diff --git a/docs/lang/fr/WEBRTC.md b/docs/lang/fr/WEBRTC.md index 47296274b3..381677b3f6 100644 --- a/docs/lang/fr/WEBRTC.md +++ b/docs/lang/fr/WEBRTC.md @@ -2,7 +2,7 @@ title: Utilisation de serveurs WebRTC ICE personnalisés dans SimpleX Chat revision: 31.01.2023 --- -| 31.01.2023 | FR, [EN](/docs/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | +| 31.01.2023 | FR, [EN](/docs/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md), [PL](/docs/lang/pl/WEBRTC.md) | # Utilisation de serveurs WebRTC ICE personnalisés dans SimpleX Chat diff --git a/docs/lang/pl/ANDROID.md b/docs/lang/pl/ANDROID.md new file mode 100644 index 0000000000..ce422ab3b5 --- /dev/null +++ b/docs/lang/pl/ANDROID.md @@ -0,0 +1,58 @@ +--- +title: Dostęp do plików w aplikacji Androidowej +revision: 07.02.2023 +--- + +| 07.02.2023 | PL, [EN](/docs/ANDROID.md), [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md) | + +# Dostęp do plików w aplikacji Androidowej + +SimpleX wykorzystuje bazy danych i przechowuje ustawienia w prywatnym katalogu w systemie Android. Katalog ten zawiera: + +- bazy danych +- wysłane i odebrane pliki +- pliki tymczasowe, które zostaną usunięte, gdy nie są już potrzebne +- ustawienia użytkownika. + +Jeśli chcesz zobaczyć, co jest przechowywane w katalogu SimpleX, musisz mieć: + +- System operacyjny oparty na systemie Unix (lub [MinGW](https://www.mingw-w64.org/downloads/) na Windowsie) +- narzędzie ADB (Android Debug Bridge) zainstalowane na komputerze ([pobierz je tutaj](https://developer.android.com/studio/releases/platform-tools) i zainstaluj) +- urządzenie podłączone przez USB lub Wi-Fi do komputera. + +## Proces: + +- otwórz SimpleX, przejdź do `Hasło do bazy danych i eksport`, włącz `Kopia zapasowa danych aplikacji`. To sprawi, że następne kroki będą działać. +- _opcjonalnie_: jeśli chcesz wyświetlić zawartość bazy danych, zmień hasło bazy danych z losowego na swoje. Aby to zrobić, zatrzymaj czat na ekranie `Hasło do bazy danych i eksport`, otwórz `Hasło do bazy danych`, wprowadź nowe hasło i potwierdź je, a następnie zatwierdź. Nie zapomnij go, w przeciwnym razie utracisz wszystkie dane w przypadku, gdy zostaniesz ponownie poproszony o hasło. +- otwórz emulator terminala (Windows CMD/Powershell nie zadziała) i zmień katalog na ten, którego chcesz użyć do przechowywania kopii zapasowej: + +```bash +cd /tmp # to tylko przykład +``` +Następne uruchom: +```bash +adb -d backup -f chat.ab -noapk chat.simplex.app && +tail -n +5 chat.ab > chat.dat && +printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - chat.dat > chat.gz && +tar -xvzf chat.gz +``` + +Teraz odblokuj urządzenie i potwierdź operację tworzenia kopii zapasowej bez użycia hasła do szyfrowania, w przeciwnym razie polecenia nie będą działać. + +Po tym kopia zapasowa powinna zostać zrobiona. Jeśli pojawi się błąd `tar: Error is not recoverable: exiting now`, ale wcześniej pojawiło się kilka nazw plików, nie martw się, wszystko jest w porządku. + +Teraz zapisane pliki będą w `./apps/chat.simplex.app/`. + +Pamiętaj, że jeśli korzystasz z nowej wersji SimpleX, bazy danych będą zaszyfrowane i nie będziesz w stanie przeglądać ich zawartości bez użycia aplikacji `sqlcipher` oraz gdy nie znasz hasła deszyfrującego (musisz najpierw zmienić je na swoje z losowo wygenerowanego w aplikacji). + +## Odszyfrowywanie baz danych + +Aby wyświetlić dane bazy danych, należy je najpierw odszyfrować. Zainstaluj `sqlcipher` używając ulubionego menedżera pakietów i uruchom następujące polecenia w katalogu z bazami danych: +```bash +sqlcipher files_chat.db +pragma key="youDecryptionPassphrase"; +# Upewnij się, że to działa +select * from users; +``` + +Jeśli zobaczysz `Parse error: no such table: users`, upewnij się, że wprowadzono prawidłowe hasło i zostało ono zmienione z losowego w aplikacji na Androida (jeśli oczywiście pobrano tę bazę danych z urządzenia z Androidem). diff --git a/docs/lang/pl/CLI.md b/docs/lang/pl/CLI.md new file mode 100644 index 0000000000..585eca3e31 --- /dev/null +++ b/docs/lang/pl/CLI.md @@ -0,0 +1,244 @@ +--- +title: Aplikacja konsolowa +revision: 31.01.2023 +--- + +| Updated 31.01.2023 | Języki: PL, [EN](/docs/CLI.md), [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md) | + +# Terminalowa (konsolowa) aplikacja SimpleX Chat dla systemów Linux/MacOS/Windows + +## Spis treści + +- [Funkcje czatu w terminalu](#funkcje-czatu-w-terminalu) +- [Instalacja](#🚀-instalacja) + - [Pobieranie klienta czatu](#pobieranie-klienta-czatu) + - [Linux i MacOS](#linux-i-macos) + - [Windows](#windows) + - [Budowanie z kodu źródłowego](#budowanie-z-kodu-źródłowego) + - [Używając dockera](#using-docker) + - [Używając Haskella na dowolnym systemie operacyjnym](#używając-haskella-na-dowolnym-systemie-operacyjnym) +- [Używanie](#używanie) + - [Używanie klienta czatu](#używanie-klienta-czatu) + - [Dostęp do serwerów wiadomości przez Tor](#dostęp-do-serwerów-wiadomości-przez-tor) + - [Jak używać czatu SimpleX](#jak-używać-czatu-simplex) + - [Grupy](#grupy) + - [Wysyłanie plików](#wysyłanie-plików) + - [Adresy kontaktowe użytkowników](#adresy-kontaktowe-użytkowników) + +## Funkcje czatu w terminalu + +- Konwersacje 1 na 1 z wieloma osobami w tym samym oknie terminala. +- Wiadomości grupowe. +- Wysyłanie plików do kontaktów i grup. +- Adresy kontaktowe użytkowników - nawiązywanie połączeń za pomocą linków kontaktowych wielokrotnego użytku. +- Wiadomości przechowywane w lokalnej bazie danych SQLite. +- Automatycznie wypełniana nazwa odbiorcy - po nawiązaniu połączenia wystarczy po prostu napisać wiadomość, aby odpowiedzieć nadawcy. +- Dostępne wstępnie skonfigurowane przykładowe serwery SMP - można też użyć [własnego serwera](https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent). +- Żadna globalna tożsamość ani nazwy użytkowników nie są widoczne dla serwera (serwerów), co zapewnia pełną prywatność kontaktów i rozmów. +- Dwie warstwy szyfrowania E2E (double-ratchet dla połączeń dwukierunkowych, przy użyciu negocjacji klucza X3DH z efemerycznymi kluczami Curve448 i NaCl crypto_box dla kolejek SMP, przy użyciu kluczy Curve25519) oraz przekazywanie kluczy odbiorców za pomocą komunikacji out-of-band (zobacz [Jak używać czatu SimpleX](#how-to-use-simplex-chat)). +- Weryfikacja integralności wiadomości (poprzez uwzględnienie hashu poprzedniej wiadomości). +- Uwierzytelnianie każdego polecenia/wiadomości przez serwery SMP za pomocą automatycznie generowanych kluczy Ed448. +- Szyfrowanie transmisji przy użyciu TLS 1.3. +- Dodatkowe szyfrowanie wiadomości z serwera SMP do odbiorcy aby utrudnić możliwość korelacji ruchu. + +Klucze publiczne biorące udział podczas wymiany kluczy nie są używane jako tożsamość, są one generowane losowo dla każdego kontaktu. + +Aby uzyskać szczegółowe informacje techniczne zobacz [używane metody szyfrowania](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#encryption-primitives-used). + + + +## 🚀 Instalacja + +### Pobieranie klienta czatu + +#### Linux i MacOS + +By **zainstalować** lub **zaktualizować** `simplex-chat`, należy uruchomić skrypt instalacyjny. Aby to zrobić, użyj następującego polecenia cURL lub Wget: + +```sh +curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash +``` + +```sh +wget -qO- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash +``` + +Po pobraniu klienta czatu można go uruchomić za pomocą polecenia `simplex-chat`. + +Możesz również ręcznie pobrać plik binarny czatu dla swojego systemu z [najnowszej stabilnej wersji](https://github.com/simplex-chat/simplex-chat/releases) i uczynić go uruchamialnym w sposób pokazany poniżej. + +```sh +chmod +x +mv ~/.local/bin/simplex-chat +``` + +(lub użyj innej preferowanej lokalizacji w `PATH`). + +Na MacOS musisz również [zezwolić Gatekeeperowi, by go uruchomić](https://support.apple.com/en-us/HT202491). + +#### Windows + +```sh +move %APPDATA%/local/bin/simplex-chat.exe +``` + +### Budowanie z kodu źródłowego + +> **Uwaga:** aby zbudować aplikację użyj [wersji stabilnej](https://github.com/simplex-chat/simplex-chat/tree/stable). + +#### Używając Dockera + +Na Linuxie, aby zbudować plik wykonywalny możesz użyć [docker build z customowym outputem](https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs): + +```shell +git clone git@github.com:simplex-chat/simplex-chat.git +cd simplex-chat +git checkout stable +DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . +``` + +> **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](Dockerfile)). + +#### Używając Haskella na dowolnym systemie operacyjnym + +1. Zainstaluj [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.3 i cabal 3.10.1.0: + +```shell +curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh +``` + +Możesz użyć polecenia `ghcup tui`, aby sprawdzić lub dodać wersje GHC i cabal. + +2. Sklonuj kod źródłowy: + +```shell +git clone git@github.com:simplex-chat/simplex-chat.git +cd simplex-chat +git checkout stable +# lub aby zbudować konkretną wersję: +# git checkout v5.3.0-beta.8 +``` + +`master` to branch deweloperski, może on zawierać niestabilny kod. + +3. Przygotowywanie systemu: + +Na Linuxie: + +```shell +apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev +cp scripts/cabal.project.local.linux cabal.project.local +``` + +Na Macu: + +``` +brew install openssl@1.1 +cp scripts/cabal.project.local.mac cabal.project.local +``` + +Może być konieczna zmiana cabal.project.local, aby wskazać poprawną lokalizację openssl + +4. Budowanie aplikacji: + +```shell +cabal update +cabal install simplex-chat +``` + +## Używanie + +### Używanie klienta czatu + +Aby uruchomić klienta, uruchom w terminalu polecenie `simplex-chat`. + +Domyślnie katalog z danymi aplikacji jest tworzony w katalogu domowym (`~/.simplex`, lub `%APPDATA%/simplex` na Windowsie), a dwa pliki danych SQLite `simplex_v1_chat.db` i `simplex_v1_agent.db` są w nim zainicjowane. + +Aby wskazać inny prefiks ścieżki dla plików bazy danych, należy użyć polecenia `-d`: + +```shell +$ simplex-chat -d alice +``` + +Uruchomienie powyższego przykładu spowoduje utworzenie plików baz danych `alice_v1_chat.db` i `alice_v1_agent.db` w bieżącym katalogu. + +Trzy domyślne serwery SMP są hostowane na Linode - są one [wstępnie skonfigurowane w aplikacji](https://github.com/simplex-chat/simplex-chat/blob/stable/src/Simplex/Chat/Options.hs#L42). + +Jeśli posiadasz własny serwer(y) SMP, możesz skonfigurować klienta poprzez opcję `-s`: + +```shell +$ simplex-chat -s smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@smp.example.com +``` + +Ciąg zakodowany w Base64url poprzedzający adres serwera to odcisk palca certyfikatu offline serwera, który jest weryfikowany przez klienta podczas handshake'a TLS. + +Po konfiguracji innego serwera w swoim kliencie nadal możesz rozmawiać z ludźmi używającymi domyślnego lub dowolnego innego serwera - ustawienie to wpływa tylko na lokalizację kolejki wiadomości podczas nawiązywania połączenia (a kolejka odpowiedzi może znajdować się na zupełnie innym serwerze, zgodnie z ustawieniami klienta rozmówcy). + +Polecenie `simplex-chat -h` pokazuje wszystkie dostępne opcje. + +### Dostęp do serwerów wiadomości przez Tor + +Zainstaluj Tor i uruchom go jako proxy SOCKS5 na porcie 9050, przykład dla MacOS: + +``` +brew install tor +brew services start tor +``` + +Użyj opcji `-x`, aby uzyskać dostęp do serwerów przez Tor: + +``` +simplex-chat -x +``` + +Możesz także użyć opcji `--socks-proxy=ipv4:port` lub `--socks-proxy=:port`, aby skonfigurować adres i port serwera proxy SOCKS5, przykładowo jeśli uruchamiasz go na innym hoście lub porcie. + +### Jak używać czatu SimpleX + +Po uruchomieniu czatu zostaniesz poproszony o podanie swojej "nazwy wyświetlanej" oraz opcjonalnej "pełnej nazwy" w celu utworzenia lokalnego profilu czatu. Nazwa wyświetlana jest aliasem, za pomocą którego kontakty mogą się do ciebie odnosić - nie jest ona unikalna i nie służy jako globalna tożsamość. Jeśli kilka kontaktów wybrało tę samą nazwę wyświetlaną, klient czatu dodaje numeryczną końcówkę (sufiks) do ich lokalnej nazwy wyświetlanej. + +Poniższy schemat przedstawia sposób łączenia się z kontaktem i wysyłania do niego wiadomości: + +
+ +
+ +Gdy już skonfigurujesz swój profil lokalny, wpisz `/c` (oznaczające `/connect`), aby utworzyć nowe połączenie i wygenerować zaproszenie. Wyślij to zaproszenie do swojego kontaktu za pośrednictwem dowolnego innego kanału komunikacji. + +Możesz utworzyć wiele zaproszeń, kilkukrotnie wpisując `/connect` i wysłać te zaproszenia do kontaktów, z którymi chcesz się połączyć. + +Zaproszenie może być użyte tylko jeden raz i nawet jeśli zostanie ono przechwycone, atakujący nie będzie mógł go użyć do wysłania do Ciebie wiadomości za pośrednictwem tej kolejki, gdy Twój kontakt potwierdzi, że połączenie zostało nawiązane. Zobacz omówienie protokołu agenta dla [formatu zaproszeń](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#connection-request). + +Kontakt, który otrzymał zaproszenie powinien wpisać `/c `, aby zaakceptować połączenie. Spowoduje to nawiązanie połączenia, a obie strony zostaną o tym powiadomione. + +Następnie można użyć komendy `@ ` do wysłania wiadomości. Możesz także po prostu zacząć pisać wiadomość, aby wysłać ją do kontaktu, który był ostatni. + +Użyj `/help` na czacie, by uzyskać listę pozostałych dostępnych komend. + +### Grupy + +Aby utworzyć grupę, użyj `/g `, a następnie dodaj do niej kontakty za pomocą `/a `. Możesz wysyłać wiadomości do grupy wpisując `# `. Użyj `/help groups`, by uzyskać listę pozostałych dostępnych komend. + +![simplex-chat](../images/groups.gif) + +> **Uwaga**: informacje o grupach nie są przechowywane na żadnym serwerze, są one zapisywane jako lista członków w bazie danych aplikacji klientów, do których będą wysyłane wiadomości. + +### Wysyłanie plików + +Możesz wysłać plik do kontaktu za pomocą `/f @ <ścieżka_do_pliku>` - odbiorca będzie musiał go zaakceptować przed rozpoczęciem wysyłania. Użyj `/help files`, by uzyskać listę pozostałych dostępnych komend. + +![simplex-chat](../images/files.gif) + +Możesz wysyłać pliki do grupy za pomocą `/f # <ścieżka_do_pliku>`. + +### Adresy kontaktowe użytkowników + +Alternatywą dla jednorazowych linków zapraszających są adresy długoterminowe. Możesz je utworzyć za pomocą `/ad` (oznaczające `/address`). Utworzony adres może być następnie udostępniony za pośrednictwem dowolnego innego kanału komunikacji i użyty przez innych użytkowników jako link do prośby o kontakt używając `/c `. + +Prośby o kontakt możesz przyjąć za pomocą komendy `/ac ` oraz odrzucić za pomocą `/rc `. + +"Długoterminowy" adres użytkownika jest długoterminowy w tym sensie, że jest to link wielokrotnego użytku - może być używany do momentu usunięcia go przez użytkownika. Po usunięciu wszystkie nawiązane połączenia pozostaną aktywne (w przeciwieństwie do tego, jak działa to w przypadku poczty e-mail, gdy zmiana adresu powoduje, że ludzie nie mogą już wysyłać do siebie wiadomości). + +Użyj `/help address`, by uzyskać listę pozostałych dostępnych komend. + +![simplex-chat](../images/user-addresses.gif) diff --git a/docs/lang/pl/CONTRIBUTING.md b/docs/lang/pl/CONTRIBUTING.md new file mode 100644 index 0000000000..4f62217479 --- /dev/null +++ b/docs/lang/pl/CONTRIBUTING.md @@ -0,0 +1,121 @@ +--- +title: Poradnik wspierania projektu +revision: 31.01.2023 +--- + +| Updated 31.01.2023 | Języki: PL, [EN](/docs/CONTRIBUTING.md) [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) | + +# Poradnik wspierania projektu + +## Kompilacja z włączonym szyfrowaniem SQLCipher + +Dodaj `cabal.project.local` do katalogu głównego projektu z lokalizacją nagłówków i bibliotek OpenSSL oraz flagą ustawiającą tryb szyfrowania: + +``` +cp scripts/cabal.project.local.mac cabal.project.local +# lub +# cp scripts/cabal.project.local.linux cabal.project.local +``` + +## OpenSSL na MacOS + +MacOS ma domyślnie zainstalowany LibreSSL, OpenSSL musi być zainstalowany, aby skompilować SimpleX z kodu źródłowego. + +OpenSSL można zainstalować za pomocą `brew install openssl@1.1` + +Będziesz musiał dodać `/opt/homebrew/opt/openssl@1.1/bin` do swojego PATH, aby wszystko działało poprawnie + + +## Branche projektu + +**W repo simplex-chat** + + +- `stable` - stabilne wydanie aplikacji, może być używane do aktualizacji poprzedniego stabilnego wydania (GHC 9.6.3). + +- `stable-android` - używane do budowania stabilnej biblioteki rdzenia Androida z Nix (GHC 8.10.7) - tylko dla Androida armv7a. + +- `stable-ios` - używane do budowania stabilnej biblioteki rdzenia iOS z Nix (GHC 8.10.7) - ten branch powinien być taki sam jak `stable-android` z wyjątkiem plików konfiguracyjnych Nix. Przestarzałe. + +- `master` - branch dla wydań wersji beta (GHC 9.6.3). + +- `master-ghc8107` - branch dla wydań wersji beta (GHC 8.10.7). Przestarzałe. + +- `master-android` - używane do budowania biblioteki rdzenia Androida w wersji beta z Nix (GHC 8.10.7) - tylko dla Androida armv7a. + +- `master-ios` - służy do budowania biblioteki rdzenia beta iOS z Nix (GHC 8.10.7). Przestarzałe. + +- `windows-ghc8107` - branch do kompilacji głównej biblioteki Windows (GHC 8.10.7). Przestarzałe? + +Branche `master-ios` i `windows-ghc8107` powinny być takie same jak `master-ghc8107` z wyjątkiem plików konfiguracyjnych Nix. + +**W repo simplexmq** + +- `master` - używa GHC 9.6.3, jego commit powinien być użyty w branchu `master` repo simplex-chat. + +- `master-ghc8107` - jego commit powinien być użyty w branchu `master-android` (i `master-ios`) repo simplex-chat. Przestarzałe. + +## Development i proces wydawania + +1. Tworzenie PR-ów do brancha `master` _tylko_ dla repozytoriów simplex-chat i simplexmq. + +2. Jeśli repozytorium simplexmq zostało zmienione, aby skompilować mobilne biblioteki rdzenia należy połączyć jego branch `master` z branchem `master-ghc8107`. + +3. Aby skompilować podstawowe biblioteki dla Androida, iOS i Windows: + +- scal branch `master` z branchem `master-android`. + +- Zaktualizuj kod, aby był kompatybilny z GHC 8.10.7 (patrz niżej). + +- push do GitHuba. + +4. Wszystkie biblioteki powinny być budowane z brancha `master`, Android armv7a - z brancha `master-android`. + +5. Aby zbudować aplikacje Desktop i CLI, należy utworzyć tag w branchu `master`, pliki APK powinny być dołączone do wydania. + +6. Po publicznym wydaniu w App Store i Play Store, scal: + +- `master` do `stable` + +- `master` do `master-android` (i skompiluj/zaktualizuj kod) + +- `master-android` do `stable-android`. + +7. Branch `master` repo simplexmq powinien zostać niezależnie scalony z branchem `stable` w wydaniach stabilnych. + + +## Różnice pomiędzy GHC 8.10.7 i GHC 9.6.3 + +1. Główna różnica związana jest z rozszerzeniem `DuplicateRecordFields`. + +W GHC 9.6.3 nie jest już możliwe określenie typu podczas korzystania z selektorów, zamiast tego używane jest rozszerzenie OverloadedRecordDot i składnia, które muszą zostać usunięte w GHC 8.10.7: + +```haskell +{-# LANGUAGE DuplicateRecordFields #-} +-- Użyj tego w GHC 9.6.3, gdy jest to potrzebne +{-# LANGUAGE OverloadedRecordDot #-} + +-- syntax GHC 9.6.3 +let x = record.field + +-- syntax GHC 8.10.7 usunięty w GHC 9.6.3 +let x = field (record :: Record) +``` + +Nadal możliwe jest określenie typu podczas korzystania ze składni aktualizacji rekordu, użyj tej reguły, aby wyłączyć ostrzeżenie kompilatora: + +```haskell +-- Użyj tego w GHC 9.6.3, gdy jest to potrzebne +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +let r' = (record :: Record) {field = value} +``` + +2. Większość funkcji monad musi być teraz importowana z `Control.Monad`, a nie z konkretnych modułów monad (np. `Control.Monad.Except`). + +```haskell +-- Użyj tego w GHC 9.6.3, gdy jest to potrzebne +import Control.Monad +``` + +[Ten PR](https://github.com/simplex-chat/simplex-chat/pull/2975/files) opisuje wszystkie różnice. diff --git a/docs/lang/pl/README.md b/docs/lang/pl/README.md new file mode 100644 index 0000000000..ba40a42d74 --- /dev/null +++ b/docs/lang/pl/README.md @@ -0,0 +1,435 @@ +[![build](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml/badge.svg?branch=stable)](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml) +[![Pobieranie z GitHuba](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases) +[![Wydanie na Githubie](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases) +[![Dołącz na Reddicie](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) +![Śledź na Mastodonie](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social) + +| 30/03/2023 | PL, [EN](/README.md), [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) | + +SimpleX logo + +# SimpleX - pierwszy komunikator bez jakichkolwiek identyfikatorów użytkowników - w 100% prywatny z założenia! + +[](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/) + +## Witamy w SimpleX Chat! + +1. 📲 [Zainstaluj aplikację](#zainstaluj-aplikację). +2. ↔️ [Połącz się z naszym zespołem](#połącz-się-z-naszym-zespołem), [dołącz do grup użytkowników](#dołącz-do-grup-użytkowników) oraz [śledź nasze aktualizacje](#śledź-nasze-aktualizacje). +3. 🤝 [Wykonaj prywatne połączenie](#wykonaj-prywatne-połączenie) ze znajomym. +4. 🔤 [Pomóż w tłumaczeniu SimpleX Chat](#pomóż-nam-przetłumaczyć-simplex-chat). +5. ⚡️ [Kontrybuuj](#kontrybuuj) i [wesprzyj nas dotacjami](#wesprzyj-nas-dotacjami). + +[Dowiedz się więcej na temat SimpleX Chat](#informacje). + +## Zainstaluj aplikację + +[Aplikacja iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084) +  +[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app) +  +[F-Droid](https://app.simplex.chat) +  +[iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu) +  +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) + +- 🖲 Chroni Twoje wiadomości i metadane - z kim rozmawiasz i kiedy. +- 🔐 Szyfrowanie end-to-end double ratchet, z dodatkową warstwą szyfrowania. +- 📱 Aplikacje mobilne dla Androida ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) oraz [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). +- 🚀 [TestFlight dla iOS](https://testflight.apple.com/join/DWuT2LQu) z nowymi funkcjami na tydzień-dwa wcześniej - **limitowane do 10,000 użytkowników**! +- 🖥 Dostępny jako terminalowa (konsolowa) [aplikacja / CLI](#zap-quick-installation-of-a-terminal-app) na Linuxa, MacOSa, Windowsa. + +## Połącz się z naszym zespołem + +Możesz połączyć się z naszym zespołem za pośrednictwem aplikacji, korzystając z przycisku "czat z deweloperami" który dostępny jest w przypadku gdy nie masz konwersacji na swoim profilu, opcji "wysyłaj pytania i pomysły" w ustawieniach aplikacji lub za pośrednictwem naszego [adresu SimpleX](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). Please connect to: + +- by zadać dowolne pytania +- by zasugerować dowolne ulepszenia +- by udostępnić nam wszystko co istotne + +Odpowiadamy na pytania manualnie, więc nie jest to natychmiastowe - może to potrwać do 24 godzin. + +Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](./docs/lang/pl/JOIN_TEAM.md), skontaktuj się z nami. + +## Dołącz do grup użytkowników + +Możesz dołączyć do grup utworzonych przez innych użytkowników za pośrednictwem nowej [usługi katalogowej](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). Nie jesteśmy odpowiedzialni za treści udostępniane w tych grupach. + +**Uwaga**: Poniższe grupy zostały utworzone, aby użytkownicy mogli zadawać pytania, zgłaszać sugestie i zadawać pytania dotyczące wyłącznie SimpleX Chat. + +Możesz również: +- krytykować aplikację i dokonywać porównań z innymi komunikatorami. +- udostępniać nowe komunikatory, które Twoim zdaniem mogą być interesujące z punktu widzenia prywatności, o ile nie spamujesz. +- udostępniać niektóre publikacje związane z prywatnością, raczej dość rzadko. +- po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](./docs/DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć. + +Musisz: +- być uprzejmym wobec innych użytkowników. +- unikać spamu (zbyt częstych wiadomości, nawet jeśli są istotne). +- unikać ataków osobistych lub wrogiego nastawienia. +- unikać dzielenia się treściami, które nie są związane z powyższymi kwestiami (co obejmuje między innymi dyskusje na temat polityki lub innych aspektów życia społecznego niż prywatność, bezpieczeństwo, technologia i komunikacja, dzielenie się treściami, które mogą zostać uznane za obraźliwe przez innych użytkowników itp.). + +Wiadomości nieprzestrzegające tych zasad będą usuwane, prawo do wysyłania wiadomości może zostać odebrane ich autorom, a dostęp nowych członków do grupy może zostać tymczasowo ograniczony, aby zapobiec ponownemu dołączeniu pod inną nazwą - nasza niedoskonała moderacja grupy nie ma obecnie lepszego rozwiązania. + +Jeśli chcesz zadać jakieś pytania, możesz dołączyć do anglojęzycznej grupy użytkowników: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) + +Istnieje również [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) dla deweloperów, którzy na bazie platformy SimpleX tworzą: + +- czatboty i automatyzacje +- integracje z innymi aplikacjami +- aplikacje społecznościowe i serwisy +- itp. + +Istnieją grupy w innych językach, na które przetłumaczyliśmy interfejs aplikacji. Grupy te służą do testowania i zadawania pytań innym użytkownikom SimpleX Chat: + +[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking). + +Możesz do nich dołączyć otwierając te linki w aplikacji lub otwierając je w przeglądarce na komputerze i skanując kod QR. + +## Śledź nasze aktualizacje + +Nasze aktualizacje i wydania publikujemy za pośrednictwem: + +- [Reddita](https://www.reddit.com/r/SimpleXChat/), [Twittera](https://twitter.com/SimpleXChat), [Lemmy](https://lemmy.ml/c/simplex), [Mastodona](https://mastodon.social/@simplex) oraz [Nostr](https://snort.social/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828). +- [profilu zespołu](#connect-to-the-team) w aplikacji SimpleX. +- [bloga](https://simplex.chat/blog/) oraz [feedu RSS](https://simplex.chat/feed.rss). +- [listy mailingowej](https://simplex.chat/#join-simplex), bardzo rzadko. + +## Wykonaj prywatne połączenie + +Aby nawiązać połączenie i rozpocząć wysyłanie wiadomości, należy udostępnić znajomemu łącze lub zeskanować kod QR z jego telefonu, osobiście lub podczas połączenia wideo. + +Kanał, za pośrednictwem którego udostępniasz link, nie musi być bezpieczny - wystarczy, że możesz potwierdzić, kto wysłał Ci wiadomość i że połączenie SimpleX zostało nawiązane. + +Wykonaj prywatne połączenie Conversation Połączenie wideo + +Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification). + +## Poradnik dla użytkownika (NOWE) + +Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](./docs/guide/README.md). + +## Pomóż nam przetłumaczyć SimpleX Chat + +Dzięki naszym użytkownikom i [Weblate](https://hosted.weblate.org/engage/simplex-chat/), aplikacje SimpleX Chat, strona internetowa i dokumenty są tłumaczone na wiele innych języków. + +Dołącz do naszych tłumaczy, aby pomóc SimpleX w rozwoju! + +|region|język |kontrybutor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) i [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[strona](https://simplex.chat)|dokumenty na GitHubie| +|:----:|:-------:|:---------:|:---------:|:---------:|:---------:| +|🇬🇧 en|English | |✓|✓|✓|✓| +|ar|العربية |[jermanuts](https://github.com/jermanuts)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/ar/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ar/)
-|[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)|| +|🇧🇬 bg|Български | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/bg/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/bg/)
[![ios app](https://hosted.weblate.org/widget/simplex-chat/ios/bg/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/bg/)||| +|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/cs/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/cs/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/cs/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/cs/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)| +|🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/de/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/de/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/de/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/de/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/de/)|| +|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)|| +|🇫🇮 fi|Suomi | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/fi/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fi/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fi/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fi/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fi/)|| +|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)| +|🇮🇱 he|עִברִית | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/he/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/he/)
-||| +|🇭🇺 hu|Magyar | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/hu/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/hu/)
-||| +|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)|| +|🇯🇵 ja|日本語 | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/ja/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ja/)|| +|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)|| +|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pl/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/pl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)||| +|🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)
-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)|| +|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)||| +|🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/th/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/th/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/th/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/th/)||| +|🇹🇷 tr|Türkçe | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/tr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/tr/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/tr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/tr/)||| +|🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)|| +|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)

[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)
 |

[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| + +Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./docs/lang/pl/TRANSLATIONS.md) i skontaktuj się z nami! + +## Kontrybuuj + +Chcielibyśmy, abyś przyczynił się do naszego rozwoju! Możesz nam pomóc: + +- [dzieląc się motywem kolorystycznym](./docs/THEMES.md), którego używasz w aplikacji na Androida! +- pisząc samouczki lub poradniki, które dotyczą hostowania serwerów, automatyzacji czatbotów itp. +- współtworząc bazy wiedzy SimpleX Chat. +- rozwijając funkcje - skontaktuj się z nami za pośrednictwem czatu, abyśmy mogli pomóc Ci zacząć. + +## Wesprzyj nas dotacjami + +Ogromne podziękowania dla wszystkich, którzy wsparli projekt SimpleX Chat! + +Na pierwszym miejscu stawiamy prywatność i bezpieczeństwo użytkowników - byłoby to niemożliwe bez waszego wsparcia. + +Naszą obietnicą wobec użytkowników jest to, że protokoły SimpleX są i pozostaną otwarte i w domenie publicznej - tak więc każdy może stworzyć przyszłe implementacje klientów i serwerów. Budujemy platformę SimpleX opartą na tych samych zasadach, co poczta e-mail i Internet, ale znacznie bardziej prywatną i bezpieczną. + +Twoje darowizny pomogą nam zebrać więcej funduszy - każda kwota, nawet koszt filiżanki kawy, będzie dla nas ogromną pomocą. + +Możesz nas wesprzeć za pomocą: + +- [GitHuba](https://github.com/sponsors/simplex-chat) - jest to dla nas wolne od prowizji. +- [OpenCollective](https://opencollective.com/simplex-chat) - pobiera prowizję, a także przyjmuje darowizny w kryptowalutach. +- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt +- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG +- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG +- USDT: + - BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 + - Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW +- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 +- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L + +Dziękuję, + +Evgeny + +Twórca SimpleX Chat. + +## Informacje + +- [Dlaczego prywatność ma znaczenie](#dlaczego-prywatność-ma-znaczenie) +- [Podejście SimpleXa do problemu prywatności i bezpieczeństwa](#podejście-simplexa-do-problemu-prywatności-i-bezpieczeństwa) + - [Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych.](#kompletna-prywatność-twojej-tożsamości-profilu-kontaktów-i-metadanych) + - [Najlepsza ochrona przed spamem i nadużyciami](#najlepsza-ochrona-przed-spamem-i-nadużyciami) + - [Pełna kontrola i bezpieczeństwo Twoich danych](#pełna-kontrola-i-bezpieczeństwo-twoich-danych) + - [Użytkownicy są właścicielami sieci SimpleX](#użytkownicy-są-właścicielami-sieci-simplex) +- [Często zadawane pytania](#często-zadawane-pytania) +- [Newsy i aktualizacje](#newsy-i-aktualizacje) +- [Szybka instalacja terminalowej wersji aplikacji](#zap-szybka-instalacja-terminalowej-wersji-aplikacji) +- [Budowa Platformy SimpleX](#budowa-platformy-simplex) +- [Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia](#prywatność-i-bezpieczeństwo-szczegóły-techniczne-i-ograniczenia) +- [Dla deweloperów](#dla-deweloperów) +- [Roadmapa](#roadmapa) +- [Ostrzeżenia, Kontakt w sprawie bezpieczeństwa, Licencja](#ostrzeżenia) + +## Dlaczego prywatność ma znaczenie + +Każdy powinien dbać o prywatność i bezpieczeństwo swojej komunikacji - nieszkodliwe rozmowy mogą narazić Cię na niebezpieczeństwo, nawet jeśli nie masz nic do ukrycia. + +Jedną z najbardziej wstrząsających historii jest doświadczenie [Mohamedou Ould Salahi](https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi). opisane w jego pamiętniku i pokazane w filmie Mauretańczyk (2021). Został on umieszczony w obozie Guantanamo, bez procesu, i był tam torturowany przez 15 lat po telefonie do swojego krewnego w Afganistanie, pod zarzutem udziału w atakach 9/11, mimo że przez poprzednie 10 lat mieszkał w Niemczech. + +Używanie szyfrowanego komunikatora end-to-end nie jest wystarczające. Powinniśmy używać komunikatorów, które zapewniają prywatność naszym powiązaniom, czyli tym z kim jesteśmy jakkolwiek połączeni. + +## Podejście SimpleXa do problemu prywatności i bezpieczeństwa + +### Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych. + +**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). + +### Najlepsza ochrona przed spamem i nadużyciami + +Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#the-best-protection-against-spam-and-abuse). + +### Pełna kontrola i bezpieczeństwo Twoich danych + +SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#complete-ownership-control-and-security-of-your-data). + +### Użytkownicy są właścicielami sieci SimpleX + +Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#users-own-simplex-network). + +## Często zadawane pytania + +1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa. + +2. _Dlaczego po prostu nie mogę używać Signal?_ Signal to scentralizowana platforma, która wykorzystuje numery telefonów do identyfikacji użytkowników i ich kontaktów. Oznacza to, że podczas gdy treść wiadomości w Signal jest chroniona solidnym szyfrowaniem end-to-end, istnieje duża ilość metadanych widocznych dla Signal - to, z kim rozmawiasz i kiedy. + +3. _Czym to się różni od Matrix, Session, Ricochet, Cwtch itp., które również nie wymagają tożsamości użytkownika?_ Mimo że te platformy nie wymagają _prawdziwej tożsamości_, to polegają na anonimowych tożsamościach użytkowników w celu dostarczania wiadomości - może to być na przykład klucz tożsamości lub liczba losowa. Korzystanie z trwałej tożsamości użytkownika, nawet anonimowej, stwarza ryzyko, że informacje na temat powiązań użytkownika staną się znane obserwatorom i/lub dostawcom usług, co może prowadzić do deanonimizacji poszczególnych użytkowników. Jeśli ten sam profil użytkownika jest używany do łączenia się z dwiema różnymi osobami za pośrednictwem dowolnego komunikatora innego niż SimpleX, wspomniane dwie osoby mogą stwierdzić, czy są rozmawiają z tą samą osobą - w wiadomościach używają tego samego identyfikatora użytkownika. W SimpleX nie ma metadanych wspólnych dla rozmów z różnymi kontaktami - jest to cecha, której nie ma żaden inny komunikator. + +## Newsy i aktualizacje + +Najnowsze i ważne wiadomości: + +[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) + +[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) + +[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md) + +[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). + +[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md). + +[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md). + +[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). + +[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). + +[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). + +[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). + +[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md). + +[All updates](./blog) + +## :zap: Szybka instalacja terminalowej wersji aplikacji + +```sh +curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash +``` + +Po pobraniu klienta czatu można go uruchomić za pomocą polecenia `simplex-chat`. + +![simplex-chat](./images/connection.gif) + +Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./docs/lang/pl/CLI.md). + +## Budowa Platformy SimpleX + +SimpleX to sieć typu klient-serwer z unikatową topologią sieciową, która wykorzystuje redundantne, jednorazowe węzły przekazywania wiadomości do asynchronicznego przekazywania wiadomości za pośrednictwem jednokierunkowych (simpleksowych) kolejek wiadomości, zapewniając anonimowość odbiorcy i nadawcy. + +W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./docs/lang/pl/SIMPLEX.md#comparison-with-p2p-messaging-protocols). + +W przeciwieństwie do sieci sfederowanych, węzły serwera **nie posiadają danych użytkowników**, **nie komunikują się ze sobą** i **nie przechowują wiadomości** po ich dostarczeniu do odbiorców. Nie ma możliwości na odkrycie pełnej listy serwerów działających w sieci SimpleX. Taka konstrukcja pozwala uniknąć problemu związanego z widocznością metadanych, z którym borykają się wszystkie sieci sfederowane i pozwala ona na lepszą ochronę przed atakami obejmującymi całą sieć. + +Informacje o użytkownikach, ich kontaktach i grupach znajdują się wyłącznie na urządzeniach klienckich. + +Przeczytaj [whitepaper SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu. + +Zobacz [Protokół Czatu SimpleX](./docs/protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md). + +## Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia + +Prace nad SimpleX Chat wciąż trwają - udostępniamy nowe ulepszenia, gdy tylko będą gotowe. To Ty musisz zdecydować, czy obecny stan jest wystarczająco dobry dla Twojego przypadku zastosowania. + +Stworzyliśmy [słownik pojęć](./docs/GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych. + +Co zostało już wprowadzone: + +1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości. +2. [Szyfrowanie end-to-end](./docs/GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519. +3. Szyfrowanie end-to-end [double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej. +4. Dodatkowa warstwa szyfrowania przy użyciu NaCL cryptobox dla wiadomości dostarczanych z serwera do odbiorcy. Warstwa ta pozwala uniknąć wspólnego szyfrogramu między wysyłanym i odbieranym ruchem serwera wewnątrz TLS (i nie ma też wspólnych identyfikatorów). +5. Kilka poziomów [content padding](./docs/GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości. +6. Wszystkie metadane wiadomości, w tym czas odebrania wiadomości przez serwer (zaokrąglony do sekundy), są wysyłane do odbiorców w zaszyfrowanej postaci, więc nawet jeśli TLS zostanie przejęty, nie można ich zobaczyć. +7. Dozwolone są tylko TLS 1.2/1.3 dla połączeń klient-serwer, z ograniczeniem do algorytmów kryptograficznych: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. +8. Aby zapobiec atakom typu replay, serwery SimpleX wymagają [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako identyfikatora sesji w każdym poleceniu klienta podpisanym kluczem efemerycznym dla każdej kolejki. +9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów. +10. Lokalne szyfrowanie bazy danych z hasłem - kontakty, grupy oraz wszystkie wysłane i odebrane wiadomości są przechowywane w postaci zaszyfrowanej. Jeśli korzystałeś z SimpleX Chat przed wersją v4.0, musisz włączyć szyfrowanie w ustawieniach aplikacji. +11. Izolacja transportu - różne połączenia TCP i obwody Tor używane są dla ruchu różnych profili użytkowników, opcjonalnie - dla różnych kontaktów i połączeń członków grupy. +12. Ręczne obracanie kolejki wiadomości w celu przeniesienia konwersacji do innego przekaźnika SMP. +13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html). +14. Szyfrowanie plików lokalnych. + +Planujemy dodać: + +1. Przekaźniki SMP nadawców i przekaźniki XFTP odbiorców w celu zmniejszenia ruchu i w celu ukrycia adresów IP przed przekaźnikami wybranymi i potencjalnie kontrolowanymi przez drugą stronę. +2. Post-kwantowa wymiana kluczy w protokole Double Ratchet. +3. Automatyczna rotacja kolejek wiadomości i redundancja. Obecnie kolejki utworzone między dwoma użytkownikami są używane, dopóki kolejka nie zostanie ręcznie zmieniona przez użytkownika lub dopóki kontakt nie zostanie usunięty. Planujemy dodać automatyczną rotację kolejek, aby te identyfikatory były tymczasowe i rotowały w oparciu o pewien harmonogram TBC (np. co X wiadomości lub co X godzin/dni). +4. "Mieszanie" wiadomości - dodanie opóźnienia do dostarczania wiadomości, w celu ochrony przed korelacją ruchu według czasu wiadomości. +5. Reprodukowalne kompilacje - ograniczeniem jest tu stos deweloperski, ale będziemy starali się rozwiązać ten problem. Użytkownicy nadal mogą tworzyć wszystkie aplikacje i usługi z kodu źródłowego. + +## Dla deweloperów + +Możesz: + +- korzystać z biblioteki SimpleX Chat w celu zintegrowania funkcji czatu z aplikacjami mobilnymi. +- tworzyć boty i usługi czatu w języku Haskell - zobacz [prosty](./apps/simplex-bot/) i bardziej [zaawansowany przykład bota czatu](./apps/simplex-bot-advanced/). +- tworzenie chat botów i usług w dowolnym języku z wykorzystaniem terminala CLI SimpleX Chat jako lokalnego serwera WebSocket. Zobacz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) i [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). +- uruchomić [simplex-chat w terminal ](./docs/lang/pl/CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki. + +Jeśli chcesz rozwijać platformę SimpleX, skontaktuj się z nami, aby uzyskać porady i wsparcie. + +Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D), aby zadawać pytania i dzielić się swoimi sukcesami. + +## Roadmapa + +- ✅ Łatwy do utworzenia serwer SimpleX z przechowywaniem wiadomości w pamięci, bez żadnych dependency. +- ✅ Terminalowa (konsolowa) wersja klienta z obsługą grup oraz plików. +- ✅ Utworzenie serwera SimpleX na Linode zaledwie jednym kliknięciem. +- ✅ Szyfrowanie end-to-end używając protokołu double-ratchet z dodatkową warstwą szyfrowania. +- ✅ Pierwsza wersja aplikacji na Androida i iOS. +- ✅ Zachowujące prywatność natychmiastowe powiadomienia na Androida przy użyciu usługi działającej w tle. +- ✅ Przykłady czatbota w Haskellu. +- ✅ v2.0 - wsparcie dla zdjęć oraz plików w aplikacjach mobilnych. +- ✅ Manualne usuwanie historii czatu. +- ✅ Szyfrowane end-to-end encrypted audio oraz wideo rozmowy przy użyciu WebRTC w aplikacjach mobilnych. +- ✅ Zachowujące prywatność natychmiastowe powiadomienia na iOS przy użyciu usługi Apple Push Notification. +- ✅ Eksportowanie oraz importowanie bazy danych czatu. +- ✅ Konwersacje grupowe w aplikacjach mmobilnych. +- ✅ Łączenie się z serwerami wiadomości przez Tor. +- ✅ Podwójne adresy serwerów w celu uzyskania dostępu do serwerów wiadomości jako ukrytych usług v3. +- ✅ Serwer czatu i SDK klienta w języku TypeScript do tworzenia interfejsów czatu, integracji i botów czatu (gotowe do ogłoszenia). +- ✅ Tryb incognito do udostępniania nowej losowej nazwy każdemu kontaktowi. +- ✅ Szyfrowanie bazy danych czatu. +- ✅ Automatyczne usuwanie historii czatu. +- ✅ Linki umożliwiające dołączanie do grup i poprawienie ich stabilności. +- ✅ Wiadomości głosowe (z opcją rezygnacji odbiorcy dla każdego kontaktu). +- ✅ Podstawowe uwierzytelnianie dla serwerów SMP (w celu autoryzacji tworzenia nowych kolejek). +- ✅ Wyświetlanie usuniętych wiadomości, pełne usuwanie wiadomości przez nadawcę (z wyrażeniem zgody przez odbiorcę dla każdego kontaktu). +- ✅ Blokowanie zrzutów ekranu i wyświetlania zawartości aplikacji w "ostatnich aplikacjach". +- ✅ Zaawansowana konfiguracja serwera. +- ✅ Znikające wiadomości (z możliwością wyboru przez odbiorcę dla każdego kontaktu). +- ✅ Wiadomości "na żywo". +- ✅ Weryfikacja kontaktu za pośrednictwem oddzielnego kanału out-of-band. +- ✅ Wiele profili użytkowników w tej samej bazie danych czatu. +- ✅ Opcjonalnie unikanie ponownego użycia tej samej sesji TCP dla wielu połączeń. +- ✅ Zachowywanie wersji roboczych wiadomości. +- ✅ Serwer plików do optymalizacji wydajnego i prywatnego wysyłania dużych plików. +- ✅ Ulepszone połączenia audio i wideo. +- ✅ Obsługa starszego systemu operacyjnego Android i 32-bitowych procesorów. +- ✅ Ukryte profile czatu. +- ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](./blog/20230301-simplex-file-transfer-protocol.md). +- ✅ Wiadomości wideo. +- ✅ Kod dostępu do aplikacji. +- ✅ Ulepszenie interfejsu Androidowej aplikacji. +- ✅ Opcjonalne alternatywne hasło dostępu. +- ✅ Reakcje na wiadomości +- ✅ Historia edytowania wiadomości +- ✅ Zmniejszenie zużycia baterii i transferu danych w dużych grupach. +- ✅ Potwierdzenie dostarczenia wiadomości (z opcją rezygnacji nadawcy dla każdego kontaktu). +- ✅ Klient desktopowy. +- ✅ Szyfrowanie plików lokalnych przechowywanych w aplikacji. +- ✅ Korzystanie z profili mobilnych z poziomu aplikacji komputerowej. +- ✅ Prywatne notatki. +- ✅ Usprawnienie wysyłania filmów (w tym szyfrowanie lokalnie przechowywanych filmów). +- ✅ Post-kwantowa wymiana kluczy w protokole double ratchet. +- 🏗 Poprawienie stabilności i zmniejszenie zużycia baterii. +- 🏗 Poprawienie odczuć dla nowych użytkowników. +- 🏗 Duże grupy, społeczności i kanały publiczne. +- 🏗 Przekaźnik dostarczania wiadomości dla nadawców (w celu ukrycia adresu IP przed serwerami odbiorców i zmniejszenia ruchu). +- Suwak prywatności i bezpieczeństwa - prosty sposób na ustawienie wszystkich ustawień za jednym zamachem. +- Redundancja i rotacja kolejek SMP (obsługiwana ręcznie). +- Dołączanie opcjonalnej wiadomości do żądania połączenia wysyłanego za pośrednictwem adresu kontaktowego. +- Ulepszona nawigacja i wyszukiwanie w konwersacji (rozwijanie i przewijanie do cytowanej wiadomości, przewijanie do wyników wyszukiwania itp.) +- Kanały/transmisje. +- Efemeryczne/znikające/jednorazowe konwersacje z istniejącymi kontaktami. +- Prywatne udostępnianie swojej lokalizacji. +- Widżety internetowe dla niestandardowej interaktywności w czatach. +- Programowalne automatyzacje czatów / reguły (automatyczne odpowiedzi / przekazywanie / usuwanie / wysyłanie, przypomnienia itp.) +- Chroniący prywatność serwer tożsamości dla opcjonalnych adresów kontaktów/grup opartych na DNS w celu uproszczenia połączenia i odnajdywania, ale nieużywany do dostarczania wiadomości: + - zachowanie wszystkich kontaktów i grup nawet w przypadku utraty domeny. + - Serwer nie posiada informacji o kontaktach i grupach użytkownika. +- Wielowęzłowe przekaźniki SMP o dużej pojemności. + +## Ostrzeżenia + +[Protokoły i model bezpieczeństwa SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) zostały poddane przeglądowi i zawierały wiele istotnych zmian i ulepszeń w wersji v1.0.0. + +Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). + +SimpleX Chat jest nadal na stosunkowo wczesnym etapie rozwoju (aplikacje mobilne zostały wydane w marcu 2022 r.), więc możesz odkryć pewne błędy i brakujące funkcje. Będziemy bardzo wdzięczni za poinformowanie nas o wszystkim, co wymaga naprawy lub ulepszenia. + +Domyślne serwery skonfigurowane w aplikacji są dostarczane na zasadzie najlepszych starań. Obecnie nie gwarantujemy żadnych umów SLA, chociaż historycznie nasze serwery miały ponad 99,9% czasu pracy. + +Nigdy nie udostępnialiśmy ani nie byliśmy proszeni o dostęp do naszych serwerów lub jakichkolwiek informacji z naszych serwerów przez osoby trzecie. Jeśli kiedykolwiek zostaniemy poproszeni o zapewnienie takiego dostępu lub informacji, będziemy postępować zgodnie z odpowiednim procesem prawnym. + +Nie rejestrujemy adresów IP użytkowników i nie przeprowadzamy żadnej korelacji ruchu na naszych serwerach. Jeśli bezpieczeństwo na poziomie transportu jest krytyczne, musisz użyć Tor lub innej podobnej sieci, aby uzyskać dostęp do serwerów wiadomości. Będziemy ulepszać aplikacje klienckie, aby zmniejszyć możliwości korelacji ruchu. + +Więcej informacji można znaleźć w [Warunkach i polityce prywatności](./PRIVACY.md). + +## Kontakt w sprawie bezpieczeństwa + +Aby zgłosić podatność wyślij nam wiadomość e-mail na adres chat@simplex.chat. Będziemy wspólnie pracować nad poprawką i ujawnieniem szczegółów. NIE zgłaszaj luk w zabezpieczeniach za pośrednictwem zgłoszeń GitHub. + +Prosimy o traktowanie wszelkich ustaleń dotyczących możliwych ataków korelacji ruchu umożliwiających skorelowanie dwóch różnych konwersacji z tym samym użytkownikiem, innych niż objęte [modelem zagrożeń](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model), jako luk w zabezpieczeniach i postępowanie zgodnie z tym procesem ujawniania. + +## Licencja + +[AGPL v3](./LICENSE) + +[iOS app](https://apps.apple.com/us/app/simplex-chat/id1605771084) +  +[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app) +  +[F-Droid](https://app.simplex.chat) +  +[iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu) +  +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) diff --git a/docs/lang/pl/SERVER.md b/docs/lang/pl/SERVER.md new file mode 100644 index 0000000000..72cb51a4bf --- /dev/null +++ b/docs/lang/pl/SERVER.md @@ -0,0 +1,485 @@ +--- +title: Hostowanie własnego serwera SMP +revision: 31.07.2023 +--- + +| Updated 05.06.2023 | Języki: PL, [EN](/docs/SERVER.md), [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | + +# Hostowanie własnego serwera SMP + +## Informacje ogólne + +Serwer SMP to serwer przekaźnikowy używany do przekazywania wiadomości w sieci SimpleX. Aplikacje SimpleX Chat mają wstępnie ustawione serwery (dla aplikacji mobilnych są to smp11, smp12 i smp14.simplex.im), ale można łatwo zmienić konfigurację aplikacji, aby korzystać z innych serwerów. + +Klienty SimpleX określają tylko, który serwer jest używany do odbierania wiadomości, oddzielnie dla każdego kontaktu (lub połączenia grupowego z członkiem grupy), a serwery te są tylko tymczasowe, ponieważ adres dostawy może ulec zmianie. + +_Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane. + +## Instalacja + +1. Najpierw zainstaluj `smp-server`: + + - Manualna instalacja (patrz niżej) + + - Półautomatyczna instalacja: + - [Oficjalny skrypt instalacyjny](https://github.com/simplex-chat/simplexmq#using-installation-script) + - [Kontener Dockera](https://github.com/simplex-chat/simplexmq#using-docker) + - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + +Instalacja ręczna wymaga kilku kroków wstępnych: + +1. Zainstaluj binarkę: + + - Używając oficjalnych binarek: + + ```sh + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server + ``` + + - Budowanie z kodu źródłowego: + + Zobacz [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution) + +2. Utwórz użytkownika i grupę dla `smp-server`: + + ```sh + sudo useradd -m smp + ``` + +3. Utwórz niezbędne katalogi i przypisz uprawnienia: + + ```sh + sudo mkdir -p /var/opt/simplex /etc/opt/simplex + sudo chown smp:smp /var/opt/simplex /etc/opt/simplex + ``` + +4. Zezwól na port `smp-server` w firewallu: + + ```sh + # Dla Ubuntu + sudo ufw allow 5223/tcp + # Dla Fedory + sudo firewall-cmd --permanent --add-port=5223/tcp && \ + sudo firewall-cmd --reload + ``` + +5. **Opcjonalnie** - Jeśli używasz dystrybucji z `systemd`, utwórz plik `/etc/systemd/system/smp-server.service` z następującą zawartością: + + ```sh + [Unit] + Description=SMP server systemd service + + [Service] + User=smp + Group=smp + Type=simple + ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS + ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex/smp-server-store.log" ] && cp "/var/opt/simplex/smp-server-store.log" "/var/opt/simplex/smp-server-store.log.bak"' + LimitNOFILE=65535 + KillSignal=SIGINT + TimeoutStopSec=infinity + + [Install] + WantedBy=multi-user.target + ``` + + I uruchom `sudo systemctl daemon-reload`. + +## Instalacja Tora + +smp-server można również zainstalować jako serwer działający w sieci [tor](https://www.torproject.org). Uruchom następujące polecenia jako użytkownik `root`. + +1. Zainstaluj Tor: + + Zakładamy, że używasz dystrybucji opartych na Ubuntu/Debian. Jeśli nie, zapoznaj się z [oficjalną dokumentacją tor](https://community.torproject.org/onion-services/setup/install/) lub poradnikiem dla Twojej dystrybucji. + + - Skonfiguruj oficjalne repozytorium Tor PPA: + + ```sh + CODENAME="$(lsb_release -c | awk '{print $2}')" + echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main + deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list + ``` + + - Zimportuj klucz repozytorium: + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null + ``` + + - Zaktualizuj indeks repozytorium: + + ```sh + apt update + ``` + + - Zainstaluj paczkę `tor`: + + ```sh + apt install -y tor deb.torproject.org-keyring + ``` + +2. Skonfiguruj tor: + + - Konfiguracja pliku: + + Otwórz konfigurację tora w wybranym edytorze (`nano`,`vim`,`emacs`, itp.): + + ```sh + vim /etc/tor/torrc + ``` + + I umieść następujące linie na dole konfiguracji. Zwróć uwagę na linie zaczynające się od `#`: są to komentarze dotyczące poszczególnych opcji. + + ```sh + # Włącz logowanie (w przeciwnym razie tor nie wyda adresu onion). + Log notice file /var/log/tor/notices.log + # Włącz routowanie single hop (2 opcje poniżej są zależne od trzeciej). Zmniejszy to opóźnienie w zamian za anonimowość (jako że tor działa równolegle z serwerem smp i adres onion będzie wyświetlany w klientach, jest to całkowicie w porządku). + SOCKSPort 0 + HiddenServiceNonAnonymousMode 1 + HiddenServiceSingleHopMode 1 + # Katalog hostów i mapowanie portów usługi ukrytej smp-server + HiddenServiceDir /var/lib/tor/simplex-smp/ + HiddenServicePort 5223 localhost:5223 + ``` + + - Utwórz katalogi: + + ```sh + mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/ + ``` + +3. Uruchom tor: + + Włącz usługę `systemd` oraz uruchom tor. Oficjalny `tor` jest nieco kłopotliwy przy pierwszym uruchomieniu i może nie utworzyć adresu hosta cebuli, więc na wszelki wypadek uruchamiamy go ponownie. + + ```sh + systemctl enable tor && systemctl start tor && systemctl restart tor + ``` + +4. Wyświetla onionowego hosta: + + Wykonaj następujące polecenie, aby wyświetlić adres onionowego hosta: + + ```sh + cat /var/lib/tor/simplex-smp/hostname + ``` + +## Configuration + +Aby zobaczyć, jakie opcje są dostępne, wykonaj `smp-server` bez flag: + +```sh +sudo su smp -c smp-server + +... +Available commands: + init Initialize server - creates /etc/opt/simplex and + /var/opt/simplex directories and configuration files + start Start server (configuration: + /etc/opt/simplex/smp-server.ini) + delete Delete configuration and log files +``` + +Możesz uzyskać dalszą pomoc, wykonując polecenie `sudo su smp -c "smp-server -h"` + +Następnie musimy skonfigurować `smp-server`: + +### Interaktywnie + +Wykonaj poniższe polecenie: + +```sh +sudo su smp -c "smp-server init" +``` + +Istnieje kilka opcji, które należy rozważyć: + +- `Enable store log to restore queues and messages on server restart (Yn):` + + Wpisz `y`, aby włączyć zapisywanie i przywracanie połączeń i wiadomości po ponownym uruchomieniu serwera. + + _Uwaga_: ważne jest, aby użyć SIGINT do ponownego uruchomienia serwera, ponieważ w przeciwnym razie niedostarczone wiadomości nie zostaną przywrócone. Połączenia zostaną przywrócone niezależnie od sposobu ponownego uruchomienia serwera, ponieważ w przeciwieństwie do wiadomości są one dodawane do dziennika append-only. + +- `Enable logging daily statistics (yN):` + + Wpisz `y`, aby włączyć logowanie statystyk w formacie CSV, mogą one być przykładowo użyte do pokazania wykresów użycia w `Grafana`. + +Statystyki te obejmują dzienną liczbę utworzonych, zabezpieczonych i usuniętych kolejek, wysłanych i odebranych wiadomości, a także dzienną, tygodniową i miesięczną liczbę aktywnych kolejek (tj. kolejek, które były używane do wysyłania wiadomości). Uważamy, że informacje te nie zawierają niczego, co pozwoliłoby na skorelowanie różnych kolejek jako należących bezpośrednio do użytkowników, ale prosimy o poufne poinformowanie nas, jeśli uważasz, że można to w jakikolwiek sposób nadużyć. + +- `Require a password to create new messaging queues?` + + Wpisz `r` lub dowolne hasło, aby zabezpieczyć hasłem `smp-server`, lub `n`, aby wyłączyć ochronę hasłem. + +- `Enter server FQDN or IP address for certificate (127.0.0.1):` + + Wprowadź domenę lub adres IP, na którym działa Twój smp-server - zostanie on zamieszczony w certyfikatach serwera, a także wyświetlony jako część adresu serwera. + +### Za pomocą opcji wiersza poleceń + +Wykonaj poniższe polecenie: + +```sh +sudo su smp -c "smp-server init -h" + +... +Available options: + -l,--store-log Enable store log for persistence + -s,--daily-stats Enable logging daily server statistics + -a,--sign-algorithm ALG Signature algorithm used for TLS certificates: + ED25519, ED448 (default: ED448) + --ip IP Server IP address, used as Common Name for TLS online + certificate if FQDN is not supplied + (default: "127.0.0.1") + -n,--fqdn FQDN Server FQDN used as Common Name for TLS online + certificate + --no-password Allow creating new queues without password + --password PASSWORD Set password to create new messaging queues + -y,--yes Non-interactive initialization using command-line + options + -h,--help Show this help text +``` + +Powinieneś określić, które flagi są potrzebne dla Twojego zastosowania, a następnie wykonać `smp-server init` z flagą `-y` dla nieinteraktywnej inicjalizacji: + +```sh +sudo su smp -c "smp-server init -y - " +``` + +Przykładowo, uruchom: + +```sh +sudo su smp -c "smp-server init -y -l --ip 192.168.1.5 --password test" +``` + +aby zainicjować konfigurację `smp-server` z: + +- przywracaniem połączeń i wiadomości po ponownym uruchomieniu serwera (flaga `-l`), +- adresem IP `192.168.1.5`, +- zabezpieczeniem `smp-server` hasłem `test`. + +--- + +Po tym instalacja jest ukończona i powinieneś zobaczyć coś takiego: + +```sh +Certificate request self-signature ok +subject=CN = 127.0.0.1 +Server initialized, you can modify configuration in /etc/opt/simplex/smp-server.ini. +Run `smp-server start` to start server. +---------- +You should store CA private key securely and delete it from the server. +If server TLS credential is compromised this key can be used to sign a new one, keeping the same server identity and established connections. +CA private key location: /etc/opt/simplex/ca.key +---------- +SMP server v3.4.0 +Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= +Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@ +``` + +Powyższy adres serwera powinien być użyty w konfiguracji klienta, a jeśli dodałeś hasło serwera, powinno ono być udostępnione innym osobom tylko wtedy, gdy chcesz zezwolić im na korzystanie z Twojego serwera do odbierania wiadomości (wszystkie Twoje kontakty będą mogły wysyłać wiadomości, ponieważ nie wymaga to hasła). Jeśli podałeś adres IP lub nazwę hosta podczas instalacji, zostanie to wyświetlone jako część adresu serwera, w przeciwnym razie zastąp `` rzeczywistymi adresami serwerów. + +## Dokumentacja + +Wszystkie niezbędne pliki dla `smp-server` znajdują się w folderze `/etc/opt/simplex/`. + +Przechowywane wiadomości, połączenia, statystyki i dziennik serwera znajdują się w folderze `/var/opt/simplex/`. + +### Adres serwera SMP + +Adres serwera SMP ma następujący format: + +``` +smp://[:]@[,] +``` + +- `` + + To odcisk palca certyfikatu Twojego `smp-server`. Odcisk palca certyfikatu możesz sprawdzić w `/etc/opt/simplex/fingerprint`. + +- **opcjonalnie** `` + + To ustawione przez Ciebie hasło Twojego `smp-server`. Możesz sprawdzić to hasło w pliku `/etc/opt/simplex/smp-server.ini`, w sekcji `[AUTH]` w polu `create_password:`. + +- ``, **optional** `` + + To skonfigurowane przez Ciebie nazwy hosta Twojego `smp-server`. Nazwy hostów możesz sprawdzić w pliku `/etc/opt/simplex/smp-server.ini`, w sekcji `[TRANSPORT]` w polu `host:`. + +### Komendy systemd + +Aby uruchomić `smp-server` przy starcie hosta, uruchom: + +```sh +sudo systemctl enable smp-server.service + +Created symlink /etc/systemd/system/multi-user.target.wants/smp-server.service → /etc/systemd/system/smp-server.service. +``` + +Aby uruchomić `smp-server`, uruchom: + +```sh +sudo systemctl start smp-server.service +``` + +Aby sprawdzić status `smp-server`, uruchom: + +```sh +sudo systemctl status smp-server.service + +● smp-server.service - SMP server + Loaded: loaded (/etc/systemd/system/smp-server.service; enabled; vendor preset: enabled) + Active: active (running) since Sat 2022-11-23 19:23:21 UTC; 1min 48s ago + Main PID: 30878 (smp-server) + CGroup: /docker/5588ab759e80546b4296a7c50ffebbb1fb7b55b8401300e9201313b720989aa8/system.slice/smp-server.service + └─30878 smp-server start + +Nov 23 19:23:21 5588ab759e80 systemd[1]: Started SMP server. +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: SMP server v3.4.0 +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@ +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Store log: /var/opt/simplex/smp-server-store.log +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Listening on port 5223 (TLS)... +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: not expiring inactive clients +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: creating new queues requires password +``` + +Aby zatrzymać `smp-server`, uruchom: + +```sh +sudo systemctl stop smp-server.service +``` + +Aby sprawdzić zawartość dziennika `smp-server`, uruchom: + +```sh +sudo journalctl -fu smp-server.service + +Nov 23 19:23:21 5588ab759e80 systemd[1]: Started SMP server. +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: SMP server v3.4.0 +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@ +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Store log: /var/opt/simplex/smp-server-store.log +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: Listening on port 5223 (TLS)... +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: not expiring inactive clients +Nov 23 19:23:21 5588ab759e80 smp-server[30878]: creating new queues requires password +``` + +### Monitorowanie + +Możesz włączyć statystyki `smp-server` dla dashboardu `Grafana` ustawiając wartość `on` w `/etc/opt/simplex/smp-server.ini`, w sekcji `[STORE_LOG]` w polu `log_stats:`. + +Logi będą przechowywane jako plik `csv` w `/var/opt/simplex/smp-server-stats.daily.log`. Pola dla pliku `csv` to: + +```sh +fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues +``` + +- `fromTime` - timestamp; data i godzina zdarzenia + +- `qCreated` - int; utworzone kolejki + +- `qSecured` - int; ustanowione kolejki + +- `qDeleted` - int; usunięte queues + +- `msgSent` - int; wysłane wiadomości + +- `msgRecv` - int; odebrane wiadomości + +- `dayMsgQueues` - int; aktywnych kolejek podczas dnia + +- `weekMsgQueues` - int; aktywnych kolejek w tygodniu + +- `monthMsgQueues` - int; aktywnych kolejek w miesiącu + +Aby zaimportować `csv` do `Grafana` należy: + +1. Zainstalować wtyczkę Grafana: [Grafana - CSV datasource](https://grafana.com/grafana/plugins/marcusolsson-csv-datasource/) + +2. Zezwolić na tryb lokalny, dołączając następujące elementy: + + ```sh + [plugin.marcusolsson-csv-datasource] + allow_local_mode = true + ``` + + ... do `/etc/grafana/grafana.ini` + +3. Dodaj źródło danych CSV: + + - W menu bocznym kliknij zakładkę Configuration (ikona koła zębatego) + - Kliknij Add data source (Dodaj źródło danych) w prawym górnym rogu zakładki Data Sources (Źródła danych). + - Wpisz "CSV" w polu wyszukiwania, aby znaleźć źródło danych CSV. + - Kliknij wynik wyszukiwania z napisem "CSV". + - W polu URL wprowadź plik wskazujący na zawartość CSV. + +4. Gotowe! Teraz możesz utworzyć własny pulpit nawigacyjny ze statystykami. + +Dalsza dokumentacja znajduje się na stronie: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/). + +# Aktualizowanie twojego serwera SMP + +Aby zaktualizować smp-server do najnowszej wersji, wybierz metodę instalacji i postępuj zgodnie z instrukcjami: + + - Manualnie + 1. Zatrzymaj serwer: + ```sh + sudo systemctl stop smp-server + ``` + 2. Zaktualizuj binarkę: + ```sh + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server + ``` + 3. Uruchom serwer: + ```sh + sudo systemctl start smp-server + ``` + + - Używając [oficjalnego skryptu instalacyjnego](https://github.com/simplex-chat/simplexmq#using-installation-script) + 1. Uruchom: + ```sh + sudo simplex-servers-update + ``` + 2. Gotowe! + + - Używając [kontenera Dockera](https://github.com/simplex-chat/simplexmq#using-docker) + 1. Zatrzymaj i usuń kontener: + ```sh + docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="\{\{.ID\}\}")) + ``` + 2. Pobierz najnowszą wersję kontenera: + ```sh + docker pull simplexchat/smp-server:latest + ``` + 3. Uruchom nowy kontener: + ```sh + docker run -d \ + -p 5223:5223 \ + -v $HOME/simplex/smp/config:/etc/opt/simplex:z \ + -v $HOME/simplex/smp/logs:/var/opt/simplex:z \ + simplexchat/smp-server:latest + ``` + + - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) + 1. Pobierz najnowsze obrazy: + ```sh + docker-compose --project-directory /etc/docker/compose/simplex pull + ``` + 2. Zrestartuj kontenery: + ```sh + docker-compose --project-directory /etc/docker/compose/simplex up -d --remove-orphans + ``` + 3. Usuń niepotrzebne obrazy: + ```sh + docker image prune + ``` + +### Konfigurowanie aplikacji do korzystania z serwera + +Aby skonfigurować aplikację do korzystania z serwera wiadomości, skopiuj jego pełny adres, w tym hasło, i dodaj go do aplikacji. Możesz używać swojego serwera razem z predefiniowanymi serwerami lub bez nich - możesz je usunąć lub wyłączyć. + +Możliwe jest również udostępnienie adresu swojego serwera znajomym, pozwalając im zeskanować kod QR z ustawień serwera - będzie on zawierał hasło serwera, dzięki czemu będą mogli również otrzymywać wiadomości za pośrednictwem twojego serwera. + +_Uwaga_: Do obsługi haseł wymagany jest serwer SMP w wersji 4.0. Jeśli już posiadasz serwer, możesz dodać hasło do niego poprzez wpisanie hasła do pliku INI serwera. + +       diff --git a/docs/lang/pl/SIMPLEX.md b/docs/lang/pl/SIMPLEX.md new file mode 100644 index 0000000000..ff7106d84c --- /dev/null +++ b/docs/lang/pl/SIMPLEX.md @@ -0,0 +1,102 @@ +--- +title: Platfoma SimpleX +revision: 07.02.2023 +--- + +| Updated 07.02.2023 | Języki: PL, [EN](/docs/SIMPLEX.md), [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) | +# Platfoma SimpleX - motywacja i porównanie + +## Problemy + +Istniejące komunikatory oraz protokoły borykają się ze wszystkimi lub kilkoma podanymi problemami: + +- Brak zachowania prywatności profilu i kontaktów użytkownika (zachowanie poufności metadanych). +- Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end](1) +- Niechciane wiadomości (spam i nadużycia). +- Brak własności danych i ich ochrony. +- Dla nietechnicznych użytkowników używanie niescentralizowanych protokołów jest skomplikowane. + +Koncentracja komunikacji na niewielkiej liczbie scentralizowanych platform sprawia, że rozwiązanie tych problemów jest dość trudne. + +## Proponowane rozwiązanie + +Proponowany zestaw protokołów pozwala rozwiązać te problemy poprzez przechowywanie zarówno wiadomości, jak i kontaktów wyłącznie na urządzeniach klienckich, redukując rolę serwerów do zwykłych przekaźników wiadomości. Wymagają one jedynie autoryzacji wiadomości wysyłanych do kolejek, ale NIE wymagają uwierzytelniania użytkowników - dzięki temu chronione są nie tylko wiadomości, ale także metadane, ponieważ użytkownicy nie mają przypisanych do siebie żadnych identyfikatorów - w przeciwieństwie do innych platform. + +Zobacz [whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu. + +## Dlaczego SimpleX + +## SimpleX podchodzi do problemu prywatności i bezpieczeństwa w unikalny sposób + +Każdy powinien zwracać uwagę na prywatność i bezpieczeństwo swojej komunikacji - nawet zwykłe rozmowy mogą narazić Cię na niebezpieczeństwo. + +### Pełna prywatność Twojej tożsamości, profilu, kontaktu i metadanych + +**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników** - nie wymaga użycia numeru telefonu (jak Signal czy Whatsapp), adresu opartego o domenę (jak email, XMPP czy Matrix), nazw użytkownika (jak Telegram), kluczy publicznych czy nawet losowych numerów (jak pozostałe komunikatory) do identyfikowania użytkowników - nie wiemy nawet ile osób używa SimpleX. + +Do dostarczania wiadomości zamiast identyfikatorów użytkowników, których używają wszystkie inne platformy, SimpleX wykorzystuje adresy jednokierunkowych (simpleksowych) kolejek wiadomości. Korzystanie z SimpleX jest jak posiadanie innego adresu e-mail lub numeru telefonu dla każdego kontaktu, ale bez kłopotów z zarządzaniem tymi wszystkimi adresami. W niedalekiej przyszłości aplikacje SimpleX będą również automatycznie zmieniać kolejki wiadomości, przenosząc konwersacje z jednego serwera na drugi, aby zapewnić użytkownikom jeszcze lepszą prywatność. + +Takie podejście chroni prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami. Prywatność komunikacji można dodatkowo zwiększyć, konfigurując dostęp do sieci w taki sposób, by łączyć się z serwerami SimpleX za pośrednictwem sieci transportowej typu overlay, np. sieci Tor. + +### Najlepsza ochrona przed spamem i nadużyciami + +Ponieważ nie masz żadnego identyfikatora na platformie SimpleX, nie można się z Tobą skontaktować, chyba że udostępnisz jednorazowy link z zaproszeniem lub opcjonalny tymczasowy adres użytkownika. Nawet przy użyciu opcjonalnych adresów użytkownika, które mogą być wykorzystywane do wysyłania spamu z prośbami o kontakt, można je zmienić lub całkowicie usunąć bez utraty jakichkolwiek połączeń (kontaktów). + +### Pełna kontrola i bezpieczeństwo Twoich danych + +SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przetrzymywane tylko tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. + +Używamy przenośnego formatu bazy danych, który może być używany na wszystkich obsługiwanych urządzeniach - wkrótce dodamy możliwość eksportu bazy danych czatu z aplikacji mobilnej, aby można było jej używać na innym urządzeniu. + +W przeciwieństwie do serwerów sieci federowanych (e-mail, XMPP lub Matrix), serwery SimpleX nie przechowują kont użytkowników, a jedynie przekazują wiadomości do odbiorców, chroniąc prywatność obu stron. Nie ma żadnych identyfikatorów ani zaszyfrowanych wiadomości występujących wspólnie z wysłanym i odbieranym ruchem serwera, dzięki dodatkowej warstwie szyfrowania dostarczanych wiadomości. Jeśli więc ktoś obserwuje ruch na serwerze, nie może łatwo określić, kto komunikuje się z kim (sprawdź [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) by dowiedzieć się o znanych atakach korelacji ruchu). + +### Użytkownicy są właścicielami sieci SimpleX + +Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. + +Platforma SimpleX korzysta z otwartego protokołu i zapewnia zestaw SDK do tworzenia czatbotów, umożliwiając implementację usług, z którymi użytkownicy mogą wchodzić w interakcje za pośrednictwem aplikacji SimpleX Chat - naprawdę nie możemy się doczekać, aby zobaczyć, jakie usługi oparte o SimpleX można stworzyć. + +Jeśli rozważasz stworzenie czegoś w oparciu o platformę SimpleX, niezależnie od tego, czy chodzi o usługi czatbotów dla użytkowników aplikacji SimpleX, czy też integrację biblioteki SimpleX Chat z aplikacjami mobilnymi, skontaktuj się z nami, aby uzyskać porady i wsparcie. + +## Porównanie z innymi protokołami + +| | SimpleX Chat | Signal, duże platformy | XMPP, Matrix | Protokoły P2P | +| :---------------------------------------------------------- | :----------------------: | :--------------------: | :-------------: | :-------------: | +| Wymaga identyfikatorów użytkownika | Nie = prywatny | Tak1 | Tak2 | Tak3 | +| Możliwość ataku MITM | Nie = bezpieczny | Tak4 | Tak | Tak | +| Polega na DNS | Nie = odporny na cenzurę | Tak | Tak | Nie | +| Pojedynczy operator lub sieć | Nie = zdecentralizowany | Tak | Nie | Tak5 | +| Scentralizowanie lub możliwość ataku obejmującego całą sieć | Nie = odporny na cenzurę | Tak | Tak2 | Tak6 | + +1. Zwykle opiera się na numerze telefonu, w niektórych przypadkach na nazwie użytkownika. +2. Bazuje na DNS. +3. Klucz publiczny lub inny globalnie unikalny identyfikator. +4. Jeśli serwery operatora zostaną przejęte. +5. Mimo że sieci P2P i sieci oparte na kryptowalutach są rozproszone, nie są w pełni zdecentralizowane - działają jako pojedyncza sieć, z pojedynczą przestrzenią nazw adresów użytkowników. +6. Sieci P2P albo mają jakiś centralny serwer, albo cała sieć może zostać przejęta - patrz następna sekcja. + +## Porównanie z komunikatorami [P2P][9] + +Istnieje kilka protokołów czatu/wiadomości P2P i implementacji, które mają na celu rozwiązanie problemu prywatności i centralizacji, ale mają one swój własny szereg problemów, które sprawiają, że są mniej niezawodne niż proponowany projekt, są bardziej skomplikowane w implementacji i analizie oraz są bardziej podatne na ataki. + +1. Sieci [P2P][9] korzystają z jakiegoś rodzaju [DHT][10] do routowania wiadomości/zapytań po sieci. Implementacje DHT mają złożone konstrukcje, muszą równoważyć niezawodność, gwarancję dostawy i czas oczekiwania. Proponowany model zapewnia zarówno większą gwarancję dostarczalności, jak i mniejsze opóźnienia (wiadomość jest przekazywana wiele razy równolegle, za każdym razem przez jeden węzeł, przy użyciu serwerów wybranych przez odbiorcę, podczas gdy w sieciach P2P wiadomość jest przekazywana przez `O(log N)` węzłów sekwencyjnie, przy użyciu węzłów wybranych przez algorytm). + +2. Proponowany model, w przeciwieństwie do większości sieci P2P, nie posiada żadnych globalnych identyfikatorów użytkowników, nawet tymczasowych. + +3. P2P samo w sobie nie rozwiązuje problemu [ataku MITM][2], a większość istniejących rozwiązań nie wykorzystuje komunikacji out-of-band do początkowej wymiany kluczy. Proponowany projekt wykorzystuje wiadomości out-of-band lub (w niektórych przypadkach) istniejące wcześniej bezpieczne i zaufane połączenia do początkowej wymiany kluczy. + +4. Implementacje P2P mogą być blokowane przez niektórych dostawców Internetu (tak jak [BitTorrent][11]). Proponowany model jest niezależny od rodzaju transmisji - może działać na standardowych protokołach sieciowych, a serwery mogą działać na tych samych domenach, co strony internetowe. + +5. Wszystkie znane sieci P2P mogą być podatne na [atak typu Sybil][12], ponieważ każdy węzeł jest wykrywalny, a sieć działa jako całość. Znane środki mające na celu zmniejszenie prawdopodobieństwa ataku typu Sybil wymagają zastosowania scentralizowanego komponentu lub kosztownego [proof of work][13]. Proponowany model, przeciwnie, nie ma możliwości wykrycia serwera - serwery nie są połączone, nie są znane sobie nawzajem i wszystkim klientom. Sieć SimpleX jest pofragmentowana i działa jako wiele odizolowanych połączeń. Uniemożliwia to ataki na całą sieć SimpleX - nawet jeśli niektóre serwery są zagrożone, inne części sieci mogą działać normalnie, a dotknięci atakiem użytkownicy mogą przełączyć się na inne serwery bez utraty kontaktów lub wiadomości. + +6. Sieci P2P są prawdopodobnie [podatne][14] na [atak DRDoS][15]. W proponowanym modelu klienci przekazują tylko ruch ze znanych zaufanych połączeń i nie mogą być wykorzystywani do odbijania i wzmacniania ruchu w całej sieci. + +[1]: https://pl.wikipedia.org/wiki/Szyfrowanie_od_ko%C5%84ca_do_ko%C5%84ca +[2]: https://pl.wikipedia.org/wiki/Atak_man_in_the_middle +[9]: https://pl.wikipedia.org/wiki/Peer-to-peer +[10]: https://pl.wikipedia.org/wiki/Rozproszona_tablica_mieszaj%C4%85ca +[11]: https://pl.wikipedia.org/wiki/BitTorrent +[12]: https://en.wikipedia.org/wiki/Sybil_attack +[13]: https://pl.wikipedia.org/wiki/Proof_of_Work +[14]: https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent +[15]: https://pl.wikipedia.org/wiki/DRDoS diff --git a/docs/lang/pl/TRANSLATIONS.md b/docs/lang/pl/TRANSLATIONS.md new file mode 100644 index 0000000000..36daa5a148 --- /dev/null +++ b/docs/lang/pl/TRANSLATIONS.md @@ -0,0 +1,104 @@ +--- +title: Współtworzenie tłumaczenia SimpleX Chat +revision: 19.03.2023 +--- + +| 19.03.2023 | PL, [EN](/docs/TRANSLATIONS.md), [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md)| + +# Współtworzenie tłumaczenia SimpleX Chat + +Dziękujemy za zainteresowanie się tłumaczeniem SimpleX Chat - to bardzo pomaga w uczynieniu go dostępnym dla szerszego grona użytkowników i naprawdę doceniamy Twoją pomoc. + +Wymaga to znacznej inwestycji czasu - większość ludzi tego początkowo nie docenia - oraz stałej opieki w miarę rozwoju aplikacji. + +Ten dokument został stworzony, po to by przyspieszyć ten proces i podzielić się kilkoma ważnymi "gafami", które odkryliśmy podczas pracy z Weblate - platformą, której używamy do tłumaczeń interfejsu. + +## Zanim rozpoczniesz tłumaczenie + +1. Utwórz konto w Weblate, używając tego samego adresu e-mail, którego używasz na platformie GitHub - dzięki temu Twój wkład będzie powiązany z kontem GitHub, co może okazać się dla Ciebie przydatne w niektórych przypadkach. Gdy tłumaczenie zostanie udostępnione użytkownikom, dodamy nazwę twojego konta do [listy tłumaczy] (https://github.com/simplex-chat/simplex-chat#translate-the-apps), chyba że poprosisz nas, abyśmy tego nie robili. + +2. Przed rozpoczęciem tłumaczenia należy podpisać prostą umowę licencyjną za pośrednictwem Weblate - ma to na celu uniknięcie konfliktów związanych z prawami własności intelektualnej. Kopia tej umowy jest również [dostępna tutaj](https://github.com/simplex-chat/cla/blob/master/CLA.md). + +3. Możemy również dodać Cię do grupy tłumaczy w przypadku jakichkolwiek pytań i aktualizacji - skontaktuj się z programistami za pośrednictwem czatu (po zainstalowaniu aplikacji lub później, poprzez "Wyślij pytania i pomysły" w ustawieniach aplikacji). + +## Proces tłumaczenia + +Najłatwiej jest najpierw przetłumaczyć aplikację na Androida, a dopiero później aplikację na iOS, ponieważ przetłumaczone ciągi Androidowej aplikacji są skonfigurowane jako słownik dla iOS. + +Kroki są następujące: + +1. [Tłumaczysz aplikację na Androida](#translating-android-app) w Weblate. +2. [Sprawdzamy i publikujemy tłumaczenia aplikacji na Androida](#releasing-android-app-translations). +3. Sprawdzasz tłumaczenia w aplikacji i poprawiasz ewentualne błędy. +4. [Tłumaczysz aplikację iOS w Weblate](#translating-ios-app). +5. Sprawdzamy i publikujemy tłumaczenia aplikacji iOS. + +### Tłumaczenie aplikacji na Androida + +1. Zacznij od [aplikacji na Androida](https://hosted.weblate.org/projects/simplex-chat/android/), zarówno podczas wykonywania najbardziej czasochłonnego tłumaczenia wstępnego, jak i dodawania ciągów później. Ze względu na to, że po pierwsze, ciągi w systemie iOS mogą pojawiać się w Weblate z pewnym opóźnieniem, ponieważ wymagają ręcznego zatwierdzenia z naszej strony, zanim będą widoczne, a po drugie, aplikacja na Androida jest skonfigurowana jako słownik dla aplikacji na iOS. 2/3 wszystkich ciągów wymaga tylko kliknięcia, aby przenieść je z Androida na iOS (nadal zajmuje to trochę czasu, Weblate niestety tego nie automatyzuje). + +2. Niektóre ciągi nie wymagają tłumaczenia, ale nadal trzeba je skopiować - w interfejsie użytkownika weblate znajduje się odpowiedni przycisk: + +weblate: copy source to translation + +3. Weblate posiada również automatyczne sugestie, które mogą przyspieszyć ten proces. Czasami mogą być używane w niezmienionej formie, a czasami wymagają edycji - kliknij, aby użyć ich w tłumaczeniach. + +4. Zwróć również uwagę na Klucz ciągu (znajduje się po prawej stronie ekranu) - może on dać ci podpowiedź, co ten ciąg oznacza, gdy jego znaczenie jest niejasne. Przykładowo, klucz dla " Dodatkowy akcent" ( nie wiadomo) to "color_primary_variant" (nieco bardziej jasne, że odnosi się do koloru używanego w aplikacji). + +5. Gdy wszystkie ciągi w aplikacji na Androida zostaną przetłumaczone, przejrzyj je, aby zapewnić spójny styl i język, tak aby te same słowa były konsekwentnie używane do podobnych działań użytkownika, tak samo jak w języku angielskim. Czasami będziesz musiał użyć różnych słów w przypadkach, gdy angielski ma tylko jedno, spróbuj użyć tych wyborów spójnie w podobnych kontekstach, aby uprościć obsługę użytkownikom końcowym. + +Prosimy również o sprawdzenie tłumaczeń przy użyciu przeglądarki Chrome i funkcji *Tłumacz na angielski* w trybie _Przeglądaj_ w weblate - tak będziemy sprawdzać tłumaczenia przed ich opublikowaniem. Popraw wszelkie błędy i dodaj komentarze w przypadkach, gdy uzasadnione jest użycie różnych tłumaczeń - znacznie przyspieszy to weryfikację. + +### Udostępnianie tłumaczeń dla aplikacji na Androida + +Gdy aplikacja na Androida zostanie przetłumaczona, poinformuj nas o tym. + +My wtedy: + - przejrzymy wszystkie tłumaczenia i zasugerujemy ewentualne poprawki - to również zajmie trochę czasu :) + - scalimy je z kodem źródłowym - w tym czasie weblate będzie ustawiony na blokadę zmian. + - stworzymy wersje beta aplikacji na iOS i Androida - możemy również dodać Cię do wewnętrznych grup testerów, abyś mógł zainstalować aplikacje przed innymi. + - udostępnimy ją naszym użytkownikom korzystającym z wersji beta - już ponad tysiąc osób korzysta z wersji beta. + - wydamy aplikację i uwzględnimy nowy język w ogłoszeniu. + +### Tłumaczenie aplikacji iOS + +1. Podczas tłumaczenia [aplikacji iOS](https://hosted.weblate.org/projects/simplex-chat/ios/) duża część ciągów jest dokładnie taka sama - można je skopiować jednym kliknięciem w sekcji słowniczka. Wskazówką jest podświetlenie całego ciągu źródłowego na żółto. Wiele innych ciągów jest bardzo do siebie podobnych, różnią się jedynie składnią lub sposobem pogrubienia czcionki - wymagają one minimalnej edycji. Istnieją jednak pewne ciągi które są unikalne dla platformy iOS - należy je przetłumaczyć osobno + +2. Przejrzyj tłumaczenia na iOS w taki sam sposób jak na Androida i daj nam znać, kiedy będą gotowe do sprawdzenia - powtórzymy ten sam proces dla aplikacji na iOS. + +Serdecznie dziękujemy! To ogromny wysiłek i wielka pomoc dla rozwoju sieci SimpleX. + +weblate: automatic suggestions + +## Częste błędy w tłumaczeniu + +1. Słowo "chat" jest używane w kilku znaczeniach, w zależności od kontekstu. Może ono oznaczać "aplikację SimpleX Chat" (np. w opcji Rozpocznij/zatrzymaj czat) lub "pojedynczą rozmowę". Jeśli nie jest to jasne, zapytaj się nas, a my dodamy więcej uwag dotyczących tłumaczenia. + +2. Prosimy o używanie liczby mnogiej i pojedynczej tak jak w oryginalnych ciągach, w przeciwnym razie może to zmienić ich znaczenie. Przykładowo, niektóre ustawienia mają zastosowanie do wszystkich kontaktów, a niektóre tylko do jednego kontaktu, będzie to mylące dla użytkownika, jeśli użyjesz liczby mnogiej w obu przypadkach. + +3. Aplikacja używa "Passcode" do zapewnienia dostępu, a nie "hasła" ("password") - w wielu językach jest to tłumaczone jako "kod dostępu". Baza danych używa "Passphrase" - w wielu językach jest to tłumaczone jako "hasło". Prosimy o spójne używanie tych słów. + +4. "Rola" użytkownika. To słowo odnosi się do zestawu uprawnień posiadanych przez użytkownika, może to być "właściciel", "administrator", "członek" lub "obserwator" (najniższe uprawnienie, które pozwala tylko na czytanie wiadomości i dodawanie reakcji na wiadomości). Tłumaczenie tego jako "tożsamość" lub "funkcja" może być nieprawidłowe. + +5. "Moderate" / "moderated" ("moderować" / "zmoderowany"). Te słowa oznaczają odpowiednio "usunięcie wiadomości innego użytkownika" i "usunięcie przez administratora". Ta funkcja jest używana, gdy członek wysyła wiadomość, która nie jest odpowiednia dla grupy. Wiele języków ma podobne słowa. + +## Jak sprawdzamy tłumaczenia + +Aby zweryfikować poprawność tłumaczeń, sprawdzamy tłumaczenia poprzez przeglądanie stron Weblate w przeglądarce Google Chrome w trybie "Tłumacz na angielski". Na przykład, aby sprawdzić niemieckie tłumaczenia interfejsu Androida, ktoś z naszego zespołu przewinął [te 68 stron] (https://hosted.weblate.org/browse/simplex-chat/android/de/). + +Nie oczekujemy, że odwrócone tłumaczenie będzie dokładnie takie samo jak oryginał, rzadko się to zdarza, ale że będzie ogólnie poprawne. + +Znacznie ułatwiłoby to recenzję, gdybyś mógł wcześniej sprawdzić to w ten sam sposób i skomentować wszystkie przypadki, w których odwrócone tłumaczenia są zupełnie inne (mogą istnieć uzasadnione przypadki). + +## Co dalej + +1. W miarę aktualizowania aplikacji będziemy publikować aktualizacje w grupie tłumaczy. Nie masz absolutnie żadnego obowiązku tłumaczenia tych dodatkowych ciągów. Niemniej jednak bardzo docenimy, jeśli to zrobisz, ponieważ sprawia to, że użytkownicy mają o wiele lepsze wrażenia, gdy polegają na Twoich tłumaczeniach, niż gdyby jakaś nowa część aplikacji nie została przetłumaczona. + +2. Możesz jeszcze bardziej pomóc w popularyzacji SimpleX w swoim kraju / grupie językowej, tłumacząc [naszą stronę internetową](https://simplex.chat) (również [przez weblate](https://hosted.weblate.org/projects/simplex-chat/website/)) i/lub [dokumenty GitHub](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang) (jest to możliwe tylko przez git)! + +3. Ponadto, jeśli chcesz być moderatorem / administratorem grupy użytkowników w swoim języku, po przetłumaczeniu aplikacji możemy hostować taką grupę - przygotowujemy wytyczne dla społeczności i dodajemy kilka narzędzi moderacyjnych do aplikacji, która zostanie wydana w wersji 4.6 w marcu. + + +Jeszcze raz bardzo dziękujemy za pomoc w rozwoju SimpleX Chat! + +Evgeny, założyciel SimpleX Chat. diff --git a/docs/lang/pl/WEBRTC.md b/docs/lang/pl/WEBRTC.md new file mode 100644 index 0000000000..d279491fb8 --- /dev/null +++ b/docs/lang/pl/WEBRTC.md @@ -0,0 +1,158 @@ +--- +title: Korzystanie z niestandardowych serwerów WebRTC ICE w SimpleX Chat +revision: 31.01.2023 +--- + +| Updated 31.01.2023 | Języki: PL, [EN](/docs/WEBRTC.md), [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) | + +# Korzystanie z niestandardowych serwerów WebRTC ICE w SimpleX Chat + +## Instalacja serwera STUN/TURN + +W tym poradniku będziemy używać najbardziej funkcjonalnej i przetestowanej w boju implementacji serwera STUN/TURN - [`coturn`](https://github.com/coturn/coturn) i [Ubuntu 20.04 LTS`](https://ubuntu.com/download/server) dystrybucji Linuksa. + +0. Uzyskaj certyfikaty `stun.$TWOJA_DOMENA` i `turn.$TWOJA_DOMENA`. + + Używamy [Let's Encrypt](https://letsencrypt.org/getting-started/). + +1. Zainstaluj pakiet `coturn` z głównego repozytorium. + +```sh +apt update && apt install coturn` +``` + +2. Odkomentuj `TURNSERVER_ENABLED=1` z `/etc/default/coturn`: + +```sh +sed -i '/TURN/s/^#//g' /etc/default/coturn +``` + +3. Skonfiguruj `coturn` w `/etc/turnserver.conf`: + + Zobacz również komentarze dotyczące poszczególnych opcji. + +```sh +# Nasłuchuj również na porcie 443 dla tls +alt-tls-listening-port=443 +# Używaj odcisków palców w komunikatach TURN +fingerprint +# Użyj mechanizmu poświadczeń długoterminowych +lt-cred-mech +# Twoje poświadczenia +user=$YOUR_LOGIN:$YOUR_PASSWORD +# Domena Twojego serwera +server-name=$YOUR_DOMAIN +# Domyślny obszar, który ma być używany dla użytkowników, gdy nie znaleziono wyraźnej relacji pochodzenie/obszar +realm=$YOUR_DOMAIN +# Ścieżka do Twoich certyfikatów. Upewnij się, że są one czytelne dla użytkownika/grupy procesu cotun. +cert=/var/lib/turn/cert.pem +pkey=/var/lib/turn/key.pem +# Użyj predefiniowanego klucza DH TLS o długości 2066 bitów +dh2066 +# Logowanie do journalctl +syslog +# Użytkownik/grupa, która będzie uruchamiać usługę coturn +proc-user=turnserver +proc-group=turnserver +# Wyłącz słabe szyfrowanie +no-tlsv1 +no-tlsv1_1 +no-tlsv1_2 +``` + +4. Uruchom i włącz serwis `coturn`: + +```sh +systemctl enable coturn && systemctl start coturn +``` + +5. Opcjonalnie, jeśli używasz firewalla `ufw`, otwórz odpowiednie porty: + +- **3478** – "czysty" TURN/STUN; +- **5349** – TURN/STUN over TLS; +- **443** – TURN/STUN over TLS, który może omijać firewalle; +- **49152:65535** – zakres portów, który Coturn będzie domyślnie wykorzystywał dla przekaźnika TURN. + +```sh +# Dla Ubuntu +sudo ufw allow 3478 && \ +sudo ufw allow 443 && \ +sudo ufw allow 5349 && \ +sudo ufw allow 49152:65535/tcp && \ +sudo ufw allow 49152:65535/udp + +# Dla Fedory +sudo firewall-cmd --permanent --add-port=443/tcp && \ +sudo firewall-cmd --permanent --add-port=443/udp && \ +sudo firewall-cmd --permanent --add-port=5349/tcp && \ +sudo firewall-cmd --permanent --add-port=5349/udp && \ +sudo firewall-cmd --permanent --add-port=49152:65535/tcp && \ +sudo firewall-cmd --permanent --add-port=49152:65535/udp && \ +sudo firewall-cmd --reload +``` + +## Konfiguracja aplikacji mobilnych + +Aby skonfigurować aplikację mobilną do korzystania z serwera: + +1. Otwórz `Ustawienia / Sieć i serwery / Serwery WebRTC ICE` i przełącz przełącznik `Konfiguruj serwery ICE`. + +2. Wprowadź wszystkie adresy serwerów w polu, po jednym na linię, na przykład jeśli serwery znajdują się na porcie 5349: + +``` +stun:stun.example.com:5349 +turn:username:password@turn.example.com:5349 +``` + +To tyle - teraz możesz wykonywać połączenia audio i wideo za pośrednictwem własnego serwera, bez udostępniania jakichkolwiek danych naszym serwerom (poza wymianą kluczy z kontaktem w szyfrowanych wiadomościach E2E). + +## Rozwiązywanie problemów + +- **Określ czy Twój serwer jest dostępny**: + + Uruchom to polecenie w terminalu: + + ```sh + ping + ``` + + Jeśli pakiety są transmitowane, serwer działa! + +- **Określ czy porty są otwarte**: + + Uruchom to polecenie w terminalu: + + ```sh + nc -zvw10 443 5349 + ``` + + Powinno się pojawić: + + ``` + Connection to 443 port [tcp/https] succeeded! + Connection to 5349 port [tcp/*] succeeded! + ``` + +- **Test połączenia STUN/TURN**: + + 1. Wejdź na [IceTest](https://icetest.info/). + + 2. W sekcji **Build up ICE Server List** dodaj: + + + + - `STUN: stun::` kliknij `Add STUN` + - `TURN: turn::`, `Username: `, `Credential: ` kliknij `Add TURN` + + Gdzie `` to 443 lub 5349. + + 3. Powinieneś zobaczyć swoje serwery w sekcji **ICE server list**. Jeśli wszystko jest skonfigurowane poprawnie, naciśnij `Start test`: + + + + 4. W sekcji **Results** powinieneś zobaczyć coś takiego: + + + + Jeśli wyniki pokazują `srflx` i `relay`, wszystko jest skonfigurowane poprawnie! + diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md index 71d5efcef7..e3d8e88ae0 100644 --- a/docs/protocol/simplex-chat.md +++ b/docs/protocol/simplex-chat.md @@ -10,7 +10,7 @@ Evgeny Poberezkin ## Abstract -SimpleX Chat Protocol is a protocol used to by SimpleX Chat clients to exchange messages. This protocol relies on lower level SimpleX protocols - SimpleX Messaging Protocol (SMP) and SimpleX Messaging Agent protocol. SimpleX Chat Protocol describes the format of messages and the client operations that should be performed when receiving such messages. +SimpleX Chat Protocol is a protocol used by SimpleX Chat clients to exchange messages. This protocol relies on lower level SimpleX protocols - SimpleX Messaging Protocol (SMP) and SimpleX Messaging Agent protocol. SimpleX Chat Protocol describes the format of messages and the client operations that should be performed when receiving such messages. ## Scope diff --git a/docs/rfcs/2024-04-01-super-peers-2.md b/docs/rfcs/2024-04-01-super-peers-2.md new file mode 100644 index 0000000000..1ebd72bc41 --- /dev/null +++ b/docs/rfcs/2024-04-01-super-peers-2.md @@ -0,0 +1,262 @@ +# Large public grups / channels + +This document describes specific design elements for the MVP of [the groups based on super-peers](./2024-03-14-super-peers.md). + +## Super-peer members + +There are two possible design approaches for super-peer members: + +1. Separate non-participating members that can only be added as a super-peers and do not have their own roles in the group, can't send their own messages or administer the group. + +2. Super-peer being a function of any member, irrespective of their role. + +While approach 1 can be simpler, it has its downsides: +- more complex migration of the existing groups - e.g., directory service cannot become a super-peer while remaining group admin (or moderator, if we add a new role). +- less usable clients - users can have desktop clients with good internet connectivity and sufficient computing resources to effectively host several medium size groups, even with SQLite database. +- it makes super-peers more like servers, being both unusable as clients and also requiring larger concurrency, and therefore increasing centralization. + +Therefore, the approach 2 looks more attractive, when super-peers must have some role in the group, and the super peers that cannot send messages will have observer role. + +As a side note, it also implies that it is beneficial to see the permission to moderate messages not as a role somewhere between Member and Admin, but as a separate privilege that admins and owners have by default for the messages from the members up to their role, but in general it's a separate member profile setting that allows to moderate messages up to a certain role. That approach would help automatic moderation as well, when moderators may be allowed to moderate messages of admins, without being able to remove members. + +Proposed protocol modifications: + +```haskell +-- this type is used in XGrpMemNew, XGrpMemIntro and XGrpMemFwd for member introductions +data MemberInfo = MemberInfo + { memberId :: MemberId, + memberRole :: GroupMemberRole, + rank :: Maybe Word8, -- new field, 0 for usual members, 1 for super-peers, allows to build additional distribution hierarhies if needed. + perms :: Maybe MemberPermissions, -- new field + v :: Maybe ChatVersionRange, + profile :: Profile + } + +-- this type is used in XGrpInv (in GroupInvitation) and XGrpLinkInv (in GroupLinkInvitation) +-- to invite members to the group +data MemberIdRole = MemberIdRole + { memberId :: MemberId, + memberRole :: GroupMemberRole, + rank :: Maybe Word8, -- new field + perms :: Maybe MemberPermissions -- new field + } + +-- new type +data MemberPermissions = MemberPermissions + { moderate :: MemberPermissionTarget -- could be extended to array in the future + } + +-- new type +data MemberPermissionTarget = MemberPermissionTarget + { maxRole :: Maybe GroupMemberRole -- if absent, can moderate all messages + } +``` + +For backwards compatibility, `admin` role implies `{moderate: {maxRole: admin}}` permission and `owner` - `{moderate: {maxRole: owner}}`, which probably can be overridden with `{moderate: {maxRole: observer}}`. + +It is also proposed to migrate `memberId` and `memberRole` to `id` and `role` in the parser (in a forward/backward compatible way), without changing serializers. + +## Group routing mode + +Irrespective of the presense of super peers in the group, the group itself has to be switched to super-peers routing at some point via a group profile update. + +```haskell +data GroupProfile = GroupProfile + { displayName :: GroupName, + fullName :: Text, + description :: Maybe Text, + image :: Maybe ImageData, + groupPreferences :: Maybe GroupPreferences, + rank :: Maybe Int, -- new field, 0 for flat groups, 1 for groups with super peers + redundancy :: Maybe GroupRedundancy, -- new field, only used when rank > 0 + consensus :: Maybe GroupConsensus -- new field, see below in + } + +-- each field defines an average number of super-peers that will deliver messages (events) related to a specific scope, +-- by default all super peers will deliver the events in that scope. +data GroupRedundancy = GroupRedundancy + { messages :: Maybe Double, -- messages and message changes, including reactions and comments + members :: Maybe Double, -- member additions and permission changes + group :: Maybe Double -- group profile and other changes + } +``` + +## Decisions about message or another event delivery + +In groups with rank 0 (current groups) all members aim to establish connections with all other members, making it hard to scale. Admins who added the members play a temporary role of forwarding messages, but only until members are connected. + +In groups with rank 1 super-peers connect to all members, but each message or event may be delivered by some rather than by all super-peers, to avoid substantial traffic increase. E.g., in groups with 2 super-peers it would be desirable to deliver each message 1.33 (4/3) times on average, while for groups with 3 super peers it can be 2 or 1.667 (5/3) times. + +The decision whether a given super peer should deliver the message would depend on these factors: +- deterministic (see below) message hash, `h`. +- receiving member ID, `r`. +- super-peer member ID, `s`. +- target message redundancy `d` (the desired number of super-peers to deliver the message). +- number of super-peers connected to member `r` (and known as connected to super-peer making the decision), `n`. +- 0-based index of the current super-peer in the sorted array of known super peer IDs, `i`. It does not require sorting all peer IDs, it's enough to count how many peers have smaller or larger IDs. + +The assumption here is that member ID is unique within the group, and that message hashes are also unique. Also, message hashes rather than sending member IDs are used to ensure that the decision is made differently for the same sender/recipient pairs, allowing recipients to identify integrity violations (e.g., if some of the super-peers decides to change the messages or fail to deliver it). + +For simplicity, all parameters are normalized to 0..1 range from their respective binary ranges. + +The algorithm to decide whether the message should be delivered by a given super-peer: + +``` +if (d >= n || n == 1) deliver; +else + prob = d/n ; delivery probability based on target delivery redundancy + point = (h + r)/2 ; as member ID and message hash are uniformly random in 0..1 range, `point` will also be uniformly random in 0..1 range + start = (1 - prob) * i / (n - 1) ; `start` for the peer with `i == 0` will be `0` and with `i == n - 1` will be `1 - prob` + end = start + prob ; `end` for peer 0 will be `prob` and for peer `n - 1` will be 1 + ; for all peers the range `start..end` will have width `prob` + if (point >= start && point < end) deliver + else skip; +``` + +This algorithm can be proven to result in target average delivery redundancy. + +It also requires all super peers to inform other super peers about: +- establishing or losing the connection with other members (e.g., when AUTH is received). +- informing about their decision to stop being super peers in advance. +- most likely using delivery receipts in communications between super-peers that would include message hashes in RCVD info. + +## Authorising administrative changes + +As members no longer send messages directly to other members, in addition to the risk of the initial MITM by admin (which is now mitigated by having multiple super-peers) there is a risk of super-peers being compromised at a later stage, and in case of administrative changes (member or group level changes) we have these options: + +1. have all members postpone these changes until they are communicated by a sufficient number of super-peers (that would require configuring consensus on the group level). +2. have admins and owners sign administrative changes with the public key included in their profile during member introduction. + +The downside of approach 1 is that administrative actions will be delayed and have to be executed not at the time the message is delivered, but at the time message is confirmed by other super-peers. That is separate and in addition to member consensus for some changes (see below). That also means that groups with one super-peer will have no defence mechanism against super-peer being compromised. That also means that groups with two super-peers will either also have no such defence (in case required consensus level is 1) or will require that both super-peers are available, and no administrative actions will be executed unless both super-peers are available. + +The downside of approach 2 is the lack of repudiation, in fact, there is a non-repudiation quality of such administrative changes as role changes, member additions and deletions, and group changes. + +Overall, while repudiation of sent messages appears as important, it seems much less important for administrative changes, and signing member and group changes while relying on Merkle DAG for message history integrity appears to be an optimal tradeoff. + +To support this functionality the `admin` and `owner` members have to add public keys to their profiles and communicate profile updates to all members (before or after group is switched to super-peers, but probably before is better). + +The middle ground here is moderation, and trade-off here is more nuanced. Practically, whether message is fully removed or marked as removed is the group policy decision, and it can be made based on whether it's more important to preserve content or to remove undesirable content without trace, and also given that super-peers are likely to be give automatic moderation capabilities anyway, preserving deniability for moderation events and relying on Merkle DAG to identify integrity violation seems a better alternative than signing moderation messages. Although it can also be a part of group policy whether to require signing moderation events. + +The change to member profile will be: + +```haskell +data Profile = Profile + { displayName :: ContactName, + fullName :: Text, + image :: Maybe ImageData, + contactLink :: Maybe ConnReqContact, + preferences :: Maybe Preferences, + authKey :: Maybe AuthKey -- new field + } + +-- this is rather ad hoc and tries to allow two things: +-- - verify that the member has the private key. +-- - allow key rotations on profile changes, without signing the whole profile change. +-- A better option could be to use certificates, although they are much large in size, +-- and also to simply sign profile changes that include key change (then prevKeySignature won't be needed) +data AuthKey = AuthKey + { key :: PublicKeyEd25519, + signature :: SignatureEd25519, -- signature of the key itself + prevKeySignature :: Maybe SignatureEd25519 + } +``` + +## Role, rank and moderation permission changes of members and group profile changes + +There are two problems with the current approach to changing permission, when a single member makes this decision: +- member whose role changes may disagree. E.g., the member may not be willing to have owner or admin role in some groups. Even more so, the member may be unable to perform super-peer functions (have rank 1). +- other owners or admins may disagree with the change or it may have been made by mistake. + +With the move to super-peers it is additionally complicated by the fact that super-peers will be forwarding these messages, and they could be compromised, thus disrupting group functioning - this risk currently exists with the existing group directory that plays admin role and in case it is compromised it can remove all members from the group. + +Part of this problem is addressed in the previous point by requiring to sign administrative changes. + +Another part is about the members agreeing to changes that affect them when additional privileges are granted, and also by requiring the consensus between group owners and admins for any privilege changes. + +There are 2 options for proposals/acceptance/approvals flow design: +1. introduce additional protocol messages for each stage, and for each type of change to complement existing messages. +2. manage these stages in orthogonal way, by adding these stages on the top level of the protocol. + +The option 2 is likely to result in a more concise protocol as it will separate approval from from the events, also allowing to include authorizations in a standardized way. This also fits well in `XGrpMsgForward` event that includes that message inside, so all authorizations will be forwarded. + +```haskell +-- fields are the number of member approvals required for actions with that role. +-- As target member acceptance is required only for privilege increases it won't be among approvals required for consensus. +-- For example, for group with consensus = {admin: 3, owners: 2}, a new member can be made admin with the decision of 3 other admins or with the decision of single owner (as owner role is higher), or the group can be removed, a new admin is added or group consensus changed with the decision of 2 admins. +-- Owners leaving without changing the consensus can result in consensus becoming unreachable - this is not different from losing a single member, and it only prevents accidental or malicious destructive actions, but does not prevent losing access. +data GroupConsensus = GroupConsensus + { admin :: Maybe Word16, -- possibly, this field is not needed + owner :: Maybe Word16 -- 1 by default + } + +-- alternatively, the type could be more flexible than that, but it can be extended if needed. + +data ChatMessage e = ChatMessage + { chatVRange :: VersionRangeChat, + msgId :: Maybe SharedMsgId, + chatMsgEvent :: ChatMsgEvent e, + stage :: Maybe MessageStage, -- new field, Nothing for broadcasted messages + bcast :: Maybe MessageBroadcast, -- new field instructing super-peers how to broadcast the message + auth :: Maybe (Either MemberApproval [MemberApproval]) -- approvals for broadcasted messages from members, Either won't be in encoding + } + +data MessageStage = MSProposed | MSApproved + +-- possibly, this could include approval stage but it is implied by the context, message needs to be approved by: +-- - the sender with the sufficient permissions +-- - the target of the change when privileges are granted +-- - other admins or owners as required by `consensus` property in group profile. +data MemberApproval = MemberApproval + { memberId :: MemberId, -- can be encoded as id? + auth :: SignatureEd25519 + } + +data MessageBroadcast = MessageBroadcast + { from :: Maybe MessageFrom, -- MFSender by default + auth :: Maybe Bool, -- whether to keep auth if present, True by default, False means to validate and remove auth + schedule :: Maybe MessageSchedule + } + +data MessageFrom = MFSender | MFApprovers + +data MessageSchedule = MessageSchedule + { deliverAt :: Maybe UTCTime, -- when received by super-peer by default + minDelay :: Maybe Int, -- seconds, added to start, 0 by default + maxDelay :: Maybe Int -- seconds, added to start, 0 by default + } + +-- e.g., to deliver at least 2 hours after received by super-peer, the MessageSchedule would be {minDelay :: 7200} +-- or, to deliver all messages 20-40 seconds after it is received, to complicate traffic correlation it would be {minDelay: 20, maxDelay: 40} +-- or, to deliver at a scheduled time {deliverAt: "2024-04-01T00:00:00Z"} +``` + +The signature is computed over deterministic message hash excluding `auth` property. This structure would also allow asking super-peers to forward the message as originating from multiple owners or admins without showing who originated the message, as long as necessary approvals are present. + +That will require extending `XGrpMsgForward` to support array of MemberId and allow UI to show multiple senders of the message: + +```haskell + | XGrpMsgForward :: [MemberId] -> ChatMessage 'Json -> UTCTime -> ChatMsgEvent 'Json +``` + +The messages send from multiple owners can have the benefit of avoiding targeted attacks on the member who originated it - it will only be known to other members sending the message, but not to other members, creating mutual responsibility for some changes and/or announcements. + +## Choosing MVP scope + +With all these ideas for improvements, the challenge is to choose some valuable MVP shippable in the shortest time. + +From the UX point of view, the most lacking parts are: + +- broadcasting messages via super-peers, including: + - protocol to accept and approve role changes is necessary, as it should not be possible to unilaterally appoint some member to be a super-peer. + - computing deterministic message hashes and signatures (and only member with the key in profile can be made super-peer). + - protocol to communicate connections with members between super-peers, yet TBD. + - algorithm to make decision whether to deliver message. +- an equivalent of Telegram channels - groups where members can only send comments and set reactions, but cannot send messages. Protocol extensions yet TBD. + +Optionally, we could include: +- Merkle tree integrity validation. +- Owners/admins consensus. +- Messages from multiple members (don't have to be owners, can be any members, irrespective of the need for consensus). +- Delayed messages. + +None of this optional list is required to launch. diff --git a/docs/rfcs/2024-04-16-ip-address-protection.md b/docs/rfcs/2024-04-16-ip-address-protection.md new file mode 100644 index 0000000000..0019ad87ca --- /dev/null +++ b/docs/rfcs/2024-04-16-ip-address-protection.md @@ -0,0 +1,81 @@ +# IP address protection, support for SMP sending proxies + +## Problem + +IP addresses of senders being visible to recipients' chosen servers, which in case of self-hosting makes it visible to recipients themselves. In case of XFTP files the issue is reversed, with IP addresses of recipients being visible to senders' chosen servers. + +## Solution + +### SMP + +- Agent to support sending proxies +- New network settings configuration "Use SMP proxies" + - Can be set to "always", "never", "for unknown servers" + - Currently configured servers to be considered as "known", including those disabled for new connections + - Initial default is "never" to allow opt-in for testing, to be changed to "for unknown servers" later + - Support in UI +- No alerts about unknown servers are required in UI +- Tor setting to not affect agent decision to use SMP proxy for each given server + +``` haskell +data UseSMPProxies + = SMPPAlways + | SMPPNever + | SMPPUnknown + deriving (Eq, Show) + +data NetworkConfig = NetworkConfig + { ... + useSMPProxies :: UseSMPProxies, + ... + } +``` + +### XFTP + +Some considerations: + +- XFTP proxying is not planned to be implemented initially + - In future it could be done via open socket and with no persistance, and would be required only for FGET command +- Currently XFTP files are automatically set for reception in some cases: + - Images and voice messages + - In background (via "set to receive") +- Agent resumes file reception (download) after app re-start, in case they weren't fully downloaded +- User may change tor setting between and during app sessions, so a file can be "accepted" when app is connected via tor, but resumed when it's no longer + +Solution: + +- Client would make a decision whether to automatically accept file, or alert user about unknown servers +- Add only_via_tor flag to agent rcv_files + pass through APIs: + - add Bool to `ReceiveFile` (with False meaning user hasn't indicated any intent regarding unknown servers) + - can either be automatically accepted, or require alerting user and asking for confirmation (see below) + - add `onlyViaTor` to `xftpReceiveFile` +- Possible scenarios: + 1. XFTP file chunks are fully available on known servers + - Do not show alert, accept automatically (images, voice messages) or without approval (other files) + - `onlyViaTor` is False + 2. Some file chunks are on unknown servers, tor is not enabled + - User accepts manually, show alert to user + - User confirms, indicating it's Ok to proceed even though IP would be visible to unknown servers + - `onlyViaTor` is False + - \* Additional user preference to never ask? -> behavior same as now + 3. Some file chunks are on unknown servers, tor is enabled + - Do not show alert, accept automatically (images, voice messages) or without approval (other files) + - `onlyViaTor` is True +- On file download: + - `onlyViaTor` equal False is ignored + - If `onlyViaTor` is True, and tor is not enabled, throw new "permanent" error + - Permanent for simplicity as this is an edge case + - File record to be removed from agent + - Could analyze whether it's remaining (not yet downloaded) chunks are on unknown servers, and only abort in this case (and instead proceed if remaining chunks are on known servers) + - This requires changing download to load data for all chunks instead of loading only current chunk + - This is another edge case of edge case, as it seems more likely that sender either used only self-hosted servers, or only preset servers + - So it seems as unnecessary complication, and it should be Ok to abort simply based on only_via_tor flag + tor setting + - Error is RFERR XFTP UNKNOWN_NO_PROXY (new constructor) +- Chat to differentiate RFERR, and make file available for re-download on UNKNOWN_NO_PROXY error, similar to when file is cancelled + - New CIFileStatus - CIFSRcvCancelledNoProxy, to differentiate in UI + - Different icon for retry, alert explaining why file was aborted + +### Trusted servers + +We also considered an idea of trusted servers, but it proved to have unnecessarily complex UX, and in case of XFTP had issues on file download continuation after restart (e.g. server "trusted" flag changing, automatically accepting with tor enabled). It requires additional consideration and may be better suited to concept of "server providers". diff --git a/docs/rfcs/2024-04-26-commercial-model.md b/docs/rfcs/2024-04-26-commercial-model.md new file mode 100644 index 0000000000..841d800715 --- /dev/null +++ b/docs/rfcs/2024-04-26-commercial-model.md @@ -0,0 +1,68 @@ +# Commercial model for SimpleX communication network + +## Problem + +SimpleX two-tier network design provides a _potential_ for a much higher degree of decentralization and privacy than a p2p network can achieve even theoretically. This is a very strong statement, and its formal proof is out of scope of this document, please see the old comparison of SimpleX network and p2p network designs [here](../SIMPLEX.md#comparison-with-p2p-messaging-protocols). + +The main downside of most, if not all, p2p networks is the lack of (or very limited) asynchronous message delivery that is critically important both for the network usability and for privacy and protection against traffic correlation attacks. + +But while two-tier network design has a _potential_ for higher privacy, this potential is hard to realize as it does not have an in-built mechanism for network operator incentives. All SimpleX network relays preset in the app are operated by SimpleX Chat Ltd., and while there are probably over 1000 self-hosted and community-hosted messaging relays - not having a single register of such relays is important for true decentralization, so we cannot know the exact number - it is probably a valid argument that a substantial part of SimpleX network traffic is provided by preset relays. Again, while we don't know the exact share, it is probably not much less than 40-50% and probably not much more than 65-75% - which is a very large share in any case for a single entity. + +For SimpleX network to achieve the level of decentralization that is designed, the applications have to be able to offer multiple network operators via the app who have commercial incentives to operate these relays. + +Traditional commercial models for consumer Internet products rely on some of these ideas: +1. Offer service for free, sell users' data. We obviously had enough of such Internet and can't wait to see it implode, lose all users, and become illegal. +2. Create a cryptocurrency or issue cryptocurrency tokens, sell them for real money, and use this money to fund service operation. While many people believe that this is the future of the Web, calling it Web3, we see it as a technological dead end, that although it provides a great platform for speculation and a playground to test such ideas as smart contracts and consensus algorithms, all of which can be used outside of the context of cryptocurrency blockchains, is not a sustainable technological foundation for a general purpose communication or information management system due to its inherent regulatory risks and distorted commercial incentives. Moxie Marlinspike wrote [a good critique of Web3](https://moxie.org/2022/01/07/web3-first-impressions.html). +3. Freemium models where some users pay for the service and, effectively, sponsor the users of the free tier. This model is free from the downsides of the previous two options, and there are many commercial services operated on this model, but this model is suboptimal for privacy in the worst case, and in the best case it still does not achieve decentralization, as whoever charges the money (the app provider) should also provide the infrastructure. + +In the mentioned critique of Web3 Moxie wrote: _We should accept the premise that people will not run their own servers by designing systems that can distribute trust without having to distribute infrastructure_. + +This statement is interesting, as it contains the correct premise - that most people do not want to and won't run their own servers - but it reaches an incorrect and limited conclusion, that the only way to provide value is by figuring out how to decentralize the trust without decentralizing infrastructure. I completely disagree that this conclusion is the only one possible, and the offered solution actually offers a way for extreme infrastructure decentralization - when not just users are distributed across providers, as happens with federated designs, but each conversation between two users is supported by infrastructure of 4-6 different independent operators - and also provides commercial incentives for these operators. + +## Solution design requirements + +So, we want to find a solution that will: +1. Offer extreme decentralization, without any kind of central authority (unlike Tor that has central authority, and unlike p2p networks that have a single addressing space) or any kind of centralized state (unlike cryptocurrency blockchains that have a centralized single state that the whole network should reach consensus about). +2. Offer low barrier for entry for infrastructure operators. +3. Offer extreme data and infrastructure portability, when infrastructure operators offer standardized primitives used by client applications - such as messaging queues and file chunks in SimpleX network. Migrating from one operator to another should not be just possible or simple, it should happen continuously and automatically, all the time, without any user action, when files and conversations move from one set of operators to another, similarly to how data moves around on SSD drives - to balance the load on the whole network, to provide reliability and redundancy, and to ensure that infrastructure operators have zero control of users and their data, and have very limited knowledge of users activity. This limited knowledge both achieves users' privacy and reduces the legal responsibility of infrastructure operators to the level of network operators. +4. Offer commercially profitable model for infrastructure operators. + +If these requirements are satisfied it would achieve a radical shift of control from infrastructure operators of today (that is achieved via SaaS model, when infrastructure operators are also software vendors, and software is provided as a service, which seems to be the root cause of corruption of Internet services - the process that Cory Doctorow refers to as [enshittification](https://www.youtube.com/watch?v=q118B_QdP2k)) to the software users, who use client software to directly consume low level infrastructure primitives of commoditized infrastructure operators that have zero control of how these primitives are used, other than pricing, that can be determined in a competitive process (e.g., via the real-time reverse auction). + +## Solution concept + +There are three types of participants in the network: +- software vendors (e.g., SimpleX Chat ltd.). +- infrastructure operators - commercial entities that have agreement with software vendors - they run server software provided by software vendors. +- users - they run client software. + +These roles can obviously overlap, but strategically it is better if they don't, e.g. we would benefit from not operating infrastructure, and users would also achieve better metadata privacy by not running the servers. + +To use SimpleX network client software needs to provision simple infrastructure resources - such as, create a messaging queue to receive the messages, create or use a session from a sending proxy to a particular destination relay, or to upload a file chunk that will be stored for a defined number of days. These resources are very cheap to provide, their price could be a very tiny fraction of a cent. + +For the users' client software to be able to provision these resources, the software vendor will issue infrastructure certificates to the users. Some number of them can be provided for free, a larger number can be sold for a monetary payment (can be in-app purchase, or cryptocurrency payment, or any other process). Software vendor will keep a private record of issued certificates. Technically, certificates can be usual cryptographic certificates that sign a public key presented by the client (while the client holds the private key). + +These certificates cannot be used as a cryptocurrency, as there is no public record of all issued certificates, and they can only be "spent" once with the infrastructure operator that has a commercial agreement with a software vendor. + +When the client wants to provision infrastructure resource it signs the request using the private key (a public counterpart of which was signed by the vendor's root certificate) and present this signed request together with the client's certificate to infrastructure operator. Operator validates the signature and certificate using vendor's root certificate and immediately provisions the resource. + +Operator then within a limited time presents its own signed request for payment to the software vendor - it would include the original request, without specifying which resource was provisioned, but only confirming that it was to this vendor. Software vendor can either confirm the acceptance and void the certificate or inform the vendor that this certificate was previously used (double spend), in which case the operator will stop provisioning the resource. + +Pros: +- this design decouples payments from resource allocation, providing better privacy, and decentralizes the infrastructure. +- this design achieves extreme provider portability and lack of control of user and user data. + +Cons: +- this design creates a strong dependence on software vendor's payment infrastructure availability - even though there can be many software vendors using this approach in a compatible way, it still creates a strong dependence of client software functioning on a single vendor. +- as described, this design allows software vendor to correlate payments of a given software user to specific infrastructure operators. + +Problem 2 can be solved by using some sort of [zero-knowledge proofs](https://en.wikipedia.org/wiki/Non-interactive_zero-knowledge_proof), where infrastructure operator can prove to software vendor that 1) they received a valid request 2) software vendor can also prove that the same certificate was not presented before, but cannot determine which certificate it was. + +Problem 1 can possibly be solved by delegating the right to issue a limited number of certificate to infrastructure operators, and making all records in the private blockchain accessible and writable by the operators using the agreements with the vendor that would also be visible on this chain, so that the operators cannot issue more certificates than agreed. In this case the payments will be made not by the software vendor to the operators but by the operators to the vendor, by compensating the buy/sell price difference. + +This is a concept of design, rather than the actual design, and the details of cryptographic primitives and consensus algorithms for this chain are out of scope. It is important that this design limits the number of operations with each certificate to three: +1. certificate issued to the user, based on the rules agreed with the software vendor. +2. certificate is used as a micro-payment for infrastructure resource (it cannot be transferred to any other user without risks of double spend). +3. certificate can only be voided by software vendor (or delegate who issued it), so it cannot be used directly as a payment. + +If we see cryptocurrency as similar to money, with similar regulations, this cryptographic primitive is close to gift cards, that have zero monetary value and can be only exchanged to a specific resource/service, thus avoiding the usual regulatory risks, and also avoiding speculative hype that could decouple the value of the certificates from the price of the infrastructure. diff --git a/package.yaml b/package.yaml index 3e187c5653..fa4d066c6b 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.6.1.1 +version: 5.7.4.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index accbebb22d..19682249e9 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -245,9 +245,10 @@ const processCommand = (function () { } const defaultIceServers: RTCIceServer[] = [ + {urls: ["stuns:stun.simplex.im:443"]}, {urls: ["stun:stun.simplex.im:443"]}, - {urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, - {urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"}, + //{urls: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"}, + {urls: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"}, ] function getCallConfig(encodedInsertableStreams: boolean, iceServers?: RTCIceServer[], relay?: boolean): CallConfig { @@ -320,7 +321,17 @@ const processCommand = (function () { } async function initializeCall(config: CallConfig, mediaType: CallMediaType, aesKey?: string): Promise { - const pc = new RTCPeerConnection(config.peerConnectionConfig) + let pc: RTCPeerConnection + try { + pc = new RTCPeerConnection(config.peerConnectionConfig) + } catch (e) { + console.log("Error while constructing RTCPeerConnection, will try without 'stuns' specified: " + e) + const withoutStuns = config.peerConnectionConfig.iceServers?.filter((elem) => + typeof elem.urls === "string" ? !elem.urls.startsWith("stuns:") : !elem.urls.some((url) => url.startsWith("stuns:")) + ) + config.peerConnectionConfig.iceServers = withoutStuns + pc = new RTCPeerConnection(config.peerConnectionConfig) + } const remoteStream = new MediaStream() const localCamera = VideoCamera.User const localStream = await getLocalMediaStream(mediaType, localCamera) @@ -576,6 +587,9 @@ const processCommand = (function () { // setupVideoElement(videos.remote) videos.local.srcObject = call.localStream videos.remote.srcObject = call.remoteStream + // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture + videos.local.play() + videos.remote.play() } async function setupEncryptionWorker(call: Call) { @@ -643,7 +657,11 @@ const processCommand = (function () { // For opus (where encodedFrame.type is not set) this is the TOC byte from // https://tools.ietf.org/html/rfc6716#section-3.1 - const capabilities = RTCRtpSender.getCapabilities("video") + // Using RTCRtpReceiver instead of RTCRtpSender, see these lines: + // - if (!is_recv_codec && !is_send_codec) { + // + if (!is_recv_codec) { + // https://webrtc.googlesource.com/src.git/+/db2f52ba88cf9f98211df2dabb3f8aca9251c4a2%5E%21/ + const capabilities = RTCRtpReceiver.getCapabilities("video") if (capabilities) { const {codecs} = capabilities const selectedCodecIndex = codecs.findIndex((c) => c.mimeType === "video/VP8") @@ -651,8 +669,15 @@ const processCommand = (function () { codecs.splice(selectedCodecIndex, 1) codecs.unshift(selectedCodec) for (const t of call.connection.getTransceivers()) { - if (t.sender.track?.kind === "video") { - t.setCodecPreferences(codecs) + // Firefox doesn't have this function implemented: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1396922 + if (t.sender.track?.kind === "video" && t.setCodecPreferences) { + try { + t.setCodecPreferences(codecs) + } catch (error) { + // Shouldn't be here but in case something goes wrong, it will allow to make a call with auto-selected codecs + console.log("Failed to set codec preferences, trying without any preferences: " + error) + } } } } @@ -673,7 +698,18 @@ const processCommand = (function () { } return } - for (const t of call.localStream.getTracks()) t.stop() + if (!call.screenShareEnabled) { + for (const t of call.localStream.getTracks()) t.stop() + } else { + // Don't stop audio track if switching to screenshare + for (const t of call.localStream.getVideoTracks()) t.stop() + // Replace new track from screenshare with old track from recording device + for (const t of localStream.getAudioTracks()) { + t.stop() + localStream.removeTrack(t) + } + for (const t of call.localStream.getAudioTracks()) localStream.addTrack(t) + } call.localCamera = camera const audioTracks = localStream.getAudioTracks() @@ -689,6 +725,7 @@ const processCommand = (function () { replaceTracks(pc, videoTracks) call.localStream = localStream videos.local.srcObject = localStream + videos.local.play() } function replaceTracks(pc: RTCPeerConnection, tracks: MediaStreamTrack[]) { @@ -738,7 +775,9 @@ const processCommand = (function () { //}, //aspectRatio: 1.33, }, - audio: true, + audio: false, + // This works with Chrome, Edge, Opera, but not with Firefox and Safari + // systemAudio: "include" } return navigator.mediaDevices.getDisplayMedia(constraints) } diff --git a/packages/simplex-chat-webrtc/src/desktop/call.html b/packages/simplex-chat-webrtc/src/desktop/call.html index 59ca2b58b0..2600062b02 100644 --- a/packages/simplex-chat-webrtc/src/desktop/call.html +++ b/packages/simplex-chat-webrtc/src/desktop/call.html @@ -10,7 +10,6 @@ diff --git a/scripts/android/build-android.sh b/scripts/android/build-android.sh index 174db533a3..97190ec103 100755 --- a/scripts/android/build-android.sh +++ b/scripts/android/build-android.sh @@ -8,9 +8,9 @@ u="$USER" tmp="$(mktemp -d -t)" folder="$tmp/simplex-chat" -nix_ver="nix-2.19.2" +nix_ver="nix-2.22.0" nix_url="https://releases.nixos.org/nix/$nix_ver/install" -nix_hash="435f0d7e11f7c7dffeeab0ec9cc55723f6d3c03352379d785633cf4ddb5caf90" +nix_hash="4fed7db867186c01ce2a2077da4a6950ed16232efbf78d0cd19700cff80559f9" nix_config="sandbox = true max-jobs = auto experimental-features = nix-command flakes" @@ -43,12 +43,13 @@ nix_setup() { } git_setup() { - [ "$folder" != "." ] && { + if [ "$folder" != "." ]; then git clone "$repo" "$folder" - } + fi - # Switch to nix-android branch - git -C "$folder" checkout "$commit" + if [ -z ${git_skip+x} ]; then + git -C "$folder" checkout "$commit" + fi } checks() { @@ -100,14 +101,10 @@ build() { sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/multiplatform/android/src/main/AndroidManifest.xml" sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts" + sed -i.bak '/tasks/Q' "$folder/apps/multiplatform/android/build.gradle.kts" for arch in $arches; do - - tag_full="$(git tag --points-at HEAD | head -n1)" - tag_version="${tag_full%%-*}" - - if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then - git checkout "${tag_version}-armv7a" + if [ "$arch" = "armv7a" ]; then android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux" android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux" else @@ -150,10 +147,6 @@ build() { zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final" rm -rf "$libs_folder/$android_arch" - - if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then - git checkout "${tag_full}" - fi done } @@ -161,15 +154,22 @@ final() { printf 'Simplex-chat was successfully compiled: %s/simplex-chat-*.apk\nDelete nix and gradle caches with "rm -rf /nix && rm $HOME/.nix* && $HOME/.gradle/caches" in case if no longer needed.\n' "$PWD" } -main() { - while getopts ":s" opt; do +pre() { + while getopts ":sg" opt; do case $opt in s) folder="." ;; + g) git_skip=1 ;; *) printf "Flag '-%s' doesn't exist.\n" "$OPTARG"; exit 1 ;; esac done + shift $(( $OPTIND - 1 )) - commit="$1"; shift 1 + + commit="${1:-HEAD}" +} + +main() { + pre "$@" git_setup checks build diff --git a/scripts/android/prepare.sh b/scripts/android/prepare.sh index 683d9cbf87..88f62af34a 100755 --- a/scripts/android/prepare.sh +++ b/scripts/android/prepare.sh @@ -3,7 +3,7 @@ # libsimplex.so and libsupport.so binaries should be in ~/Downloads folder in their directories based on archive name mkdir -p ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/ rm ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/* -unzip -o ~/Downloads/pkg-aarch64-android-libsupport.zip -d ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a +unzip -o ~/Downloads/pkg-aarch64-android-libsupport.zip -d ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/ unzip -o ~/Downloads/pkg-aarch64-android-libsimplex.zip -d ./apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a/ mkdir -p ./apps/multiplatform/common/src/commonMain/cpp/android/libs/armeabi-v7a/ diff --git a/scripts/desktop/build-desktop-mac-ci.sh b/scripts/desktop/build-desktop-mac-ci.sh index 07a3db9c8e..f4c0bdfb98 100755 --- a/scripts/desktop/build-desktop-mac-ci.sh +++ b/scripts/desktop/build-desktop-mac-ci.sh @@ -8,7 +8,7 @@ echo "desktop.mac.signing.keychain=/tmp/simplex.keychain" >> apps/multiplatform/ echo "desktop.mac.notarization.apple_id=$APPLE_SIMPLEX_NOTARIZATION_APPLE_ID" >> apps/multiplatform/local.properties echo "desktop.mac.notarization.password=$APPLE_SIMPLEX_NOTARIZATION_PASSWORD" >> apps/multiplatform/local.properties echo "desktop.mac.notarization.team_id=5NN7GUYB6T" >> apps/multiplatform/local.properties -echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode - > /tmp/simplex.keychain +echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode -o /tmp/simplex.keychain scripts/desktop/build-lib-mac.sh cd apps/multiplatform diff --git a/scripts/ios/export-localizations.sh b/scripts/ios/export-localizations.sh index cc6eed25a9..88555ba70b 100755 --- a/scripts/ios/export-localizations.sh +++ b/scripts/ios/export-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en bg cs de es fi fr it ja nl pl ru uk zh-Hans ) +langs=( en bg cs de es fi fr hu it ja nl pl ru th tr uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" diff --git a/scripts/ios/import-localizations.sh b/scripts/ios/import-localizations.sh index 4bf33f7dbd..e323865f2d 100755 --- a/scripts/ios/import-localizations.sh +++ b/scripts/ios/import-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en bg cs de es fi fr it ja nl pl ru th tr uk zh-Hans ) +langs=( en bg cs de es fi fr hu it ja nl pl ru th tr uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index d2701dc45f..40cfbdb8e0 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd" = "08l00ay1ibz7skhlpfjp6z2821zpfd0kxplwdm6zc63m2f6za7cv"; + "https://github.com/simplex-chat/simplexmq.git"."769e54db76fc33a04085410cdf78ae77ed9717b6" = "01bwy6hnkpp51jzdym8880462003npgzb2lns5946nww2smybrk6"; "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 fb0635abad..86b2b9668e 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: 5.6.1.1 +version: 5.7.4.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -140,6 +140,8 @@ library Simplex.Chat.Migrations.M20240228_pq Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id Simplex.Chat.Migrations.M20240324_custom_data + Simplex.Chat.Migrations.M20240402_item_forwarded + Simplex.Chat.Migrations.M20240430_ui_theme Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared @@ -174,6 +176,8 @@ library Simplex.Chat.Terminal.Output Simplex.Chat.Types Simplex.Chat.Types.Preferences + Simplex.Chat.Types.Shared + Simplex.Chat.Types.UITheme Simplex.Chat.Types.Util Simplex.Chat.Util Simplex.Chat.View @@ -574,6 +578,7 @@ test-suite simplex-chat-test ChatTests.ChatList ChatTests.Direct ChatTests.Files + ChatTests.Forward ChatTests.Groups ChatTests.Local ChatTests.Profiles diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 7948875180..d56e731246 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -80,17 +80,19 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util -import Simplex.Chat.Util (encryptFile, shuffle) +import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle) +import qualified Simplex.Chat.Util as U import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import qualified Simplex.FileTransfer.Description as FD import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError) +import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) -import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) @@ -101,7 +103,7 @@ import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF -import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKNoPQ, pattern IKPQOff, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -227,6 +229,7 @@ newChatController connNetworkStatuses <- atomically TM.empty subscriptionMode <- newTVarIO SMSubscribe chatLock <- newEmptyTMVarIO + entityLocks <- atomically TM.empty sndFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty currentCalls <- atomically TM.empty @@ -246,8 +249,8 @@ newChatController showLiveItems <- newTVarIO False encryptLocalFiles <- newTVarIO False tempDirectory <- newTVarIO optTempDirectory + assetsDirectory <- newTVarIO Nothing contactMergeEnabled <- newTVarIO True - pqExperimentalEnabled <- newTVarIO PQSupportOff pure ChatController { firstTime, @@ -263,6 +266,7 @@ newChatController connNetworkStatuses, subscriptionMode, chatLock, + entityLocks, sndFiles, rcvFiles, currentCalls, @@ -282,9 +286,9 @@ newChatController showLiveItems, encryptLocalFiles, tempDirectory, + assetsDirectory, logFilePath = logFile, - contactMergeEnabled, - pqExperimentalEnabled + contactMergeEnabled } where configServers :: DefaultAgentServers @@ -310,6 +314,40 @@ newChatController userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p)) userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') +withChatLock :: String -> CM a -> CM a +withChatLock name action = asks chatLock >>= \l -> withLock l name action + +withEntityLock :: String -> 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 name = withEntityLock name . CLInvitation +{-# INLINE withInvitationLock #-} + +withConnectionLock :: String -> Int64 -> CM a -> CM a +withConnectionLock name = withEntityLock name . CLConnection +{-# INLINE withConnectionLock #-} + +withContactLock :: String -> ContactId -> CM a -> CM a +withContactLock name = withEntityLock name . CLContact +{-# INLINE withContactLock #-} + +withGroupLock :: String -> GroupId -> CM a -> CM a +withGroupLock name = withEntityLock name . CLGroup +{-# INLINE withGroupLock #-} + +withUserContactLock :: String -> Int64 -> CM a -> CM a +withUserContactLock name = withEntityLock name . CLUserContact +{-# INLINE withUserContactLock #-} + +withFileLock :: String -> Int64 -> CM a -> CM a +withFileLock name = withEntityLock name . CLFile +{-# INLINE withFileLock #-} + activeAgentServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p) activeAgentServers ChatConfig {defaultServers} p = fromMaybe (cfgServers p defaultServers) @@ -370,7 +408,7 @@ subscribeUsers onlyNeeded users = do subscribe vr us subscribe vr us' where - subscribe :: (PQSupport -> VersionRangeChat) -> [User] -> CM' () + subscribe :: VersionRangeChat -> [User] -> CM' () subscribe vr = mapM_ $ runExceptT . subscribeUserConnections vr onlyNeeded Agent.subscribeConnections startFilesToReceive :: [User] -> CM' () @@ -454,7 +492,7 @@ processChatCommand cmd = chatVersionRange >>= (`processChatCommand'` cmd) {-# INLINE processChatCommand #-} -processChatCommand' :: (PQSupport -> VersionRangeChat) -> ChatCommand -> CM ChatResponse +processChatCommand' :: VersionRangeChat -> ChatCommand -> CM ChatResponse processChatCommand' vr = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do @@ -594,22 +632,19 @@ processChatCommand' vr = \case createDirectoryIfMissing True rf chatWriteVar remoteHostsFolder $ Just rf ok_ + -- has to be called before StartChat + APISetAppFilePaths cfg -> do + setFolder filesFolder $ appFilesFolder cfg + setFolder tempDirectory $ appTempFolder cfg + setFolder assetsDirectory $ appAssetsFolder cfg + mapM_ (setFolder remoteHostsFolder) $ appRemoteHostsFolder cfg + ok_ + where + setFolder sel f = do + createDirectoryIfMissing True f + chatWriteVar sel $ Just f APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_ - APISetPQEncryption onOff -> chatWriteVar pqExperimentalEnabled onOff >> ok_ - APISetContactPQ ctId pqEnc -> withUser $ \user -> do - ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user ctId - case activeConn of - Just conn@Connection {connId, pqSupport, pqEncryption} - | pqEncryption == pqEnc -> pure $ CRContactPQAllowed user ct pqEnc - | otherwise -> do - let pqSup = PQSupport $ pqEnc == PQEncOn || pqSupport == PQSupportOn - conn' = conn {pqSupport = pqSup, pqEncryption = pqEnc} :: Connection - ct' = ct {activeConn = Just conn'} :: Contact - withStore' $ \db -> updateConnSupportPQ db connId pqSup pqEnc - pure $ CRContactPQAllowed user ct' pqEnc - Nothing -> throwChatError $ CEContactNotActive ct - SetContactPQ cName pqEnc -> withContactName cName (`APISetContactPQ` pqEnc) APIExportArchive cfg -> checkChatStopped $ lift (exportArchive cfg) >> ok_ ExportArchive -> do ts <- liftIO getCurrentTime @@ -668,107 +703,30 @@ processChatCommand' vr = \case [] -> pure Nothing memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses _ -> pure Nothing - pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses} - APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> withChatLock "sendMessage" $ case cType of - CTDirect -> do - ct@Contact {contactId, contactUsed} <- withStore $ \db -> getContact db vr user chatId - assertDirectAllowed user MDSnd ct XMsgNew_ - unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct - if isVoice mc && not (featureAllowed SCFVoice forUser ct) - then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) - else do - (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer ct - timed_ <- sndContactCITimed live ct itemTTL - (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ - (msg, _) <- sendDirectContactMessage user ct (XMsgNew msgContainer) - ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live - forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) - pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) - where - setupSndFileTransfer :: Contact -> CM (Maybe (FileInvitation, CIFile 'MDSnd)) - setupSndFileTransfer ct = forM file_ $ \file -> do - fileSize <- checkSndFile file - xftpSndFileTransfer user file fileSize 1 $ CGContact ct - prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> CM (MsgContainer, Maybe (CIQuote 'CTDirect)) - prepareMsg fInv_ timed_ = case quotedItemId_ of - Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) - Just quotedItemId -> do - CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- - withStore $ \db -> getDirectChatItem db user chatId quotedItemId - (origQmc, qd, sent) <- quoteData qci - let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} - qmc = quoteContent mc origQmc file - quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} - pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) - where - quoteData :: ChatItem c d -> CM (MsgContent, CIQDirection 'CTDirect, Bool) - quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote - quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) - quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) - quoteData _ = throwChatError CEInvalidQuote - CTGroup -> do - g@(Group gInfo _) <- withStore $ \db -> getGroup db vr user chatId - assertUserGroupRole gInfo GRAuthor - send g - where - send g@(Group gInfo@GroupInfo {groupId} ms) - | isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) = notAllowedError GFVoice - | not (isVoice mc) && isJust file_ && not (groupFeatureAllowed SGFFiles gInfo) = notAllowedError GFFiles - | otherwise = do - (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms) - timed_ <- sndGroupCITimed live gInfo itemTTL - (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ timed_ live - (msg, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) - ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live - withStore' $ \db -> - forM_ sentToMembers $ \GroupMember {groupMemberId} -> - createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew - forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) - pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f)) - setupSndFileTransfer :: Group -> Int -> CM (Maybe (FileInvitation, CIFile 'MDSnd)) - setupSndFileTransfer g n = forM file_ $ \file -> do - fileSize <- checkSndFile file - xftpSndFileTransfer user file fileSize n $ CGGroup g + forwardedFromChatItem <- getForwardedFromItem user ci + pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem} + where + getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem) + getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of + Just (CIFFContact _ _ (Just ctId) (Just fwdItemId)) -> + Just <$> withStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId) fwdItemId) + Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) -> + Just <$> withStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId) + _ -> pure Nothing + APISendMessage (ChatRef cType chatId) live itemTTL cm -> withUser $ \user -> case cType of + CTDirect -> + withContactLock "sendMessage" chatId $ + sendContactContentMessage user chatId live itemTTL cm Nothing + CTGroup -> + withGroupLock "sendMessage" chatId $ + sendGroupContentMessage user chatId live itemTTL cm Nothing CTLocal -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - where - xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd) - xftpSndFileTransfer user file fileSize n contactOrGroup = do - (fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup - case contactOrGroup of - CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> - withStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr - CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user)) - where - -- we are not sending files to pending members, same as with inline files - saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = - when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $ - withStore' $ - \db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr - saveMemberFD _ = pure () - pure (fInv, ciFile) - APICreateChatItem folderId (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> do - forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported" - nf <- withStore $ \db -> getNoteFolder db user folderId - createdAt <- liftIO getCurrentTime - let content = CISndMsgContent mc - let cd = CDLocalSnd nf - ciId <- createLocalChatItem user cd content createdAt - ciFile_ <- forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do - fsFilePath <- lift $ toFSFilePath filePath - fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs - chunkSize <- asks $ fileChunkSize . config - withStore' $ \db -> do - fileId <- createLocalFile CIFSSndStored db user nf ciId createdAt cf fileSize chunkSize - pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} - let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing Nothing False createdAt Nothing createdAt - pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci - APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> withChatLock "updateChatItem" $ case cType of - CTDirect -> do + APICreateChatItem folderId cm -> withUser $ \user -> + createNoteFolderContentItem user folderId cm Nothing + APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> case cType of + CTDirect -> withContactLock "updateChatItem" chatId $ do ct@Contact {contactId} <- withStore $ \db -> getContact db vr user chatId assertDirectAllowed user MDSnd ct XMsgUpdate_ cci <- withStore $ \db -> getDirectCIWithReactions db user ct itemId @@ -784,13 +742,14 @@ processChatCommand' vr = \case currentTs <- liftIO getCurrentTime when changed $ addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId + let edited = itemLive /= Just True + updateDirectChatItem' db user contactId ci (CISndMsgContent mc) edited live Nothing $ Just msgId startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') else pure $ CRChatItemNotChanged user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate - CTGroup -> do + CTGroup -> withGroupLock "updateChatItem" chatId $ do Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db vr user chatId assertUserGroupRole gInfo GRAuthor cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId @@ -806,7 +765,8 @@ processChatCommand' vr = \case currentTs <- liftIO getCurrentTime when changed $ addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId + let edited = itemLive /= Just True + updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) @@ -820,15 +780,15 @@ processChatCommand' vr = \case | otherwise -> withStore' $ \db -> do currentTs <- getCurrentTime addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) + ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) True pure $ CRChatItemUpdated user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci') _ -> throwChatError CEInvalidChatItemUpdate CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> withChatLock "deleteChatItem" $ case cType of - CTDirect -> do - (ct, CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}}) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, editable) of + APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> case cType of + CTDirect -> withContactLock "deleteChatItem" chatId $ do + (ct, CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, deletable}}) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId + case (mode, msgDir, itemSharedMsgId, deletable) of (CIDMInternal, _, _, _) -> deleteDirectCI user ct ci True False (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do assertDirectAllowed user MDSnd ct XMsgDel_ @@ -837,10 +797,10 @@ processChatCommand' vr = \case then deleteDirectCI user ct ci True False else markDirectCIDeleted user ct ci msgId True =<< liftIO getCurrentTime (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete - CTGroup -> do + CTGroup -> withGroupLock "deleteChatItem" chatId $ do Group gInfo ms <- withStore $ \db -> getGroup db vr user chatId - CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}} <- withStore $ \db -> getGroupChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, editable) of + CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, deletable}} <- withStore $ \db -> getGroupChatItem db user chatId itemId + case (mode, msgDir, itemSharedMsgId, deletable) of (CIDMInternal, _, _, _) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier @@ -852,7 +812,7 @@ processChatCommand' vr = \case deleteLocalCI user nf ci True False CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withChatLock "deleteChatItem" $ do + APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do Group gInfo@GroupInfo {membership} ms <- withStore $ \db -> getGroup db vr user gId CChatItem _ ci@ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}} <- withStore $ \db -> getGroupChatItem db user gId itemId case (chatDir, itemSharedMsgId) of @@ -862,44 +822,46 @@ processChatCommand' vr = \case (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId delGroupChatItem user gInfo ci msgId (Just membership) (_, _) -> throwChatError CEInvalidChatItemDelete - APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> withChatLock "chatItemReaction" $ case cType of + APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> case cType of CTDirect -> - withStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case - (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do - unless (featureAllowed SCFReactions forUser ct) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) - unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") - rs <- withStore' $ \db -> getDirectReactions db ct itemSharedMId True - checkReactionAllowed rs - (SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing reaction add - createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do - setDirectReaction db ct itemSharedMId True reaction add msgId createdAt - liftIO $ getDirectCIReactions db ct itemSharedMId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction - pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + withContactLock "chatItemReaction" chatId $ + withStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case + (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do + unless (featureAllowed SCFReactions forUser ct) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + unless (ciReactionAllowed ci) $ + throwChatError (CECommandError "reaction not allowed - chat item has no content") + rs <- withStore' $ \db -> getDirectReactions db ct itemSharedMId True + checkReactionAllowed rs + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing reaction add + createdAt <- liftIO getCurrentTime + reactions <- withStore' $ \db -> do + setDirectReaction db ct itemSharedMId True reaction add msgId createdAt + liftIO $ getDirectCIReactions db ct itemSharedMId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction + pure $ CRChatItemReaction user add r + _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" CTGroup -> - withStore (\db -> (,) <$> getGroup db vr user chatId <*> getGroupChatItem db user chatId itemId) >>= \case - (Group g@GroupInfo {membership} ms, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do - unless (groupFeatureAllowed SGFReactions g) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) - unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") - let GroupMember {memberId = itemMemberId} = chatItemMember g ci - rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True - checkReactionAllowed rs - (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) - createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do - setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt - liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction - pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + withGroupLock "chatItemReaction" chatId $ + withStore (\db -> (,) <$> getGroup db vr user chatId <*> getGroupChatItem db user chatId itemId) >>= \case + (Group g@GroupInfo {membership} ms, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do + unless (groupFeatureAllowed SGFReactions g) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + unless (ciReactionAllowed ci) $ + throwChatError (CECommandError "reaction not allowed - chat item has no content") + let GroupMember {memberId = itemMemberId} = chatItemMember g ci + rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True + checkReactionAllowed rs + (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) + createdAt <- liftIO getCurrentTime + reactions <- withStore' $ \db -> do + setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt + liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction + pure $ CRChatItemReaction user add r + _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" CTLocal -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" @@ -909,6 +871,111 @@ processChatCommand' vr = \case throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") when (add && length rs >= maxMsgReactions) $ throwChatError (CECommandError "too many reactions") + APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId -> withUser $ \user -> case toCType of + CTDirect -> do + (cm, ciff) <- prepareForward user + withContactLock "forwardChatItem, to contact" toChatId $ + sendContactContentMessage user toChatId False Nothing cm ciff + CTGroup -> do + (cm, ciff) <- prepareForward user + withGroupLock "forwardChatItem, to group" toChatId $ + sendGroupContentMessage user toChatId False Nothing cm ciff + CTLocal -> do + (cm, ciff) <- prepareForward user + createNoteFolderContentItem user toChatId cm ciff + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + where + prepareForward :: User -> CM (ComposedMessage, Maybe CIForwardedFrom) + prepareForward user = case fromCType of + CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do + (ct, CChatItem _ ci) <- withStore $ \db -> do + ct <- getContact db vr user fromChatId + cci <- getDirectChatItem db user fromChatId itemId + pure (ct, cci) + (mc, mDir) <- forwardMC ci + file <- forwardCryptoFile ci + let ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) mDir (Just fromChatId) (Just itemId)) + pure (ComposedMessage file Nothing mc, ciff) + where + forwardName :: Contact -> ContactName + forwardName Contact {profile = LocalProfile {displayName, localAlias}} + | localAlias /= "" = localAlias + | otherwise = displayName + CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do + (gInfo, CChatItem _ ci) <- withStore $ \db -> do + gInfo <- getGroupInfo db vr user fromChatId + cci <- getGroupChatItem db user fromChatId itemId + pure (gInfo, cci) + (mc, mDir) <- forwardMC ci + file <- forwardCryptoFile ci + let ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) mDir (Just fromChatId) (Just itemId)) + pure (ComposedMessage file Nothing mc, ciff) + where + forwardName :: GroupInfo -> ContactName + forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName + CTLocal -> do + (CChatItem _ ci) <- withStore $ \db -> getLocalChatItem db user fromChatId itemId + (mc, _) <- forwardMC ci + file <- forwardCryptoFile ci + let ciff = forwardCIFF ci Nothing + pure (ComposedMessage file Nothing mc, ciff) + CTContactRequest -> throwChatError $ CECommandError "not supported" + CTContactConnection -> throwChatError $ CECommandError "not supported" + where + forwardMC :: ChatItem c d -> CM (MsgContent, MsgDirection) + forwardMC ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidForward + forwardMC ChatItem {content = CISndMsgContent fmc} = pure (fmc, MDSnd) + forwardMC ChatItem {content = CIRcvMsgContent fmc} = pure (fmc, MDRcv) + forwardMC _ = throwChatError CEInvalidForward + forwardCIFF :: ChatItem c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom + forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of + Nothing -> ciff + Just CIFFUnknown -> ciff + Just prevCIFF -> Just prevCIFF + forwardCryptoFile :: ChatItem c d -> CM (Maybe CryptoFile) + forwardCryptoFile ChatItem {file = Nothing} = pure Nothing + forwardCryptoFile ChatItem {file = Just ciFile} = case ciFile of + CIFile {fileName, fileStatus, fileSource = Just fromCF@CryptoFile {filePath}} + | ciFileLoaded fileStatus -> + chatReadVar filesFolder >>= \case + Nothing -> + ifM (doesFileExist filePath) (pure $ Just fromCF) (throwChatError CEForwardNoFile) + Just filesFolder -> do + let fsFromPath = filesFolder filePath + ifM + (doesFileExist fsFromPath) + ( do + fsNewPath <- liftIO $ filesFolder `uniqueCombine` fileName + liftIO $ B.writeFile fsNewPath "" -- create empty file + encrypt <- chatReadVar encryptLocalFiles + cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing + let toCF = CryptoFile fsNewPath cfArgs + -- to keep forwarded file in case original is deleted + liftIOEither $ runExceptT $ withExceptT (ChatError . CEInternalError . show) $ copyCryptoFile (fromCF {filePath = fsFromPath} :: CryptoFile) toCF + pure $ Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile) + ) + (throwChatError CEForwardNoFile) + _ -> throwChatError CEForwardNoFile + copyCryptoFile :: CryptoFile -> CryptoFile -> ExceptT CF.FTCryptoError IO () + copyCryptoFile fromCF@CryptoFile {filePath = fsFromPath, cryptoArgs = fromArgs} toCF@CryptoFile {cryptoArgs = toArgs} = do + fromSizeFull <- getFileSize fsFromPath + let fromSize = fromSizeFull - maybe 0 (const $ toInteger C.authTagSize) fromArgs + CF.withFile fromCF ReadMode $ \fromH -> + CF.withFile toCF WriteMode $ \toH -> do + copyChunks fromH toH fromSize + forM_ fromArgs $ \_ -> CF.hGetTag fromH + forM_ toArgs $ \_ -> liftIO $ CF.hPutTag toH + where + copyChunks :: CF.CryptoFileHandle -> CF.CryptoFileHandle -> Integer -> ExceptT CF.FTCryptoError IO () + copyChunks r w size = do + let chSize = min size U.chunkSize + chSize' = fromIntegral chSize + size' = size - chSize + ch <- liftIO $ CF.hGet r chSize' + when (B.length ch /= chSize') $ throwError $ CF.FTCEFileIOError "encrypting file: unexpected EOF" + liftIO . CF.hPut w $ LB.fromStrict ch + when (size' > 0) $ copyChunks r w size' APIUserRead userId -> withUserId userId $ \user -> withStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId APIChatRead (ChatRef cType chatId) fromToIds -> withUser $ \_ -> case cType of @@ -959,7 +1026,7 @@ processChatCommand' vr = \case CTDirect -> do ct <- withStore $ \db -> getContact db vr user chatId filesInfo <- withStore' $ \db -> getContactFileInfo db user ct - withChatLock "deleteChat direct" . procCmd $ do + withContactLock "deleteChat direct" chatId . procCmd $ do cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo let doSendDel = contactReady ct && contactActive ct && notify @@ -971,7 +1038,7 @@ processChatCommand' vr = \case withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct withStore $ \db -> deleteContact db user ct pure $ CRContactDeleted user ct - CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do + CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId deleteAgentConnectionAsync user acId withStore' $ \db -> deletePendingContactConnection db userId chatId @@ -983,7 +1050,7 @@ processChatCommand' vr = \case canDelete = isOwner || not (memberCurrent membership) unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo - withChatLock "deleteChat group" . procCmd $ do + withGroupLock "deleteChat group" chatId . procCmd $ do cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo let doSendDel = memberActive membership && isOwner @@ -1038,28 +1105,29 @@ processChatCommand' vr = \case CTLocal -> do nf <- withStore $ \db -> getNoteFolder db user chatId filesInfo <- withStore' $ \db -> getNoteFolderFileInfo db user nf - withChatLock "clearChat local" . procCmd $ do - deleteFilesLocally filesInfo - withStore' $ \db -> deleteNoteFolderFiles db userId nf - withStore' $ \db -> deleteNoteFolderCIs db user nf - pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) + deleteFilesLocally filesInfo + withStore' $ \db -> deleteNoteFolderFiles db userId nf + withStore' $ \db -> deleteNoteFolderCIs db user nf + pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - APIAcceptContact incognito connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do + APIAcceptContact incognito connReqId -> withUser $ \_ -> do (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withStore $ \db -> getContactRequest' db connReqId - ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl - -- [incognito] generate profile to send, create connection with incognito profile - incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - ct <- acceptContactRequest user cReq incognitoProfile contactUsed - pure $ CRAcceptingContactRequest user ct - APIRejectContact connReqId -> withUser $ \user -> withChatLock "rejectContact" $ do - cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- + withUserContactLock "acceptContact" userContactLinkId $ do + ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl + -- [incognito] generate profile to send, create connection with incognito profile + incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + ct <- acceptContactRequest user cReq incognitoProfile contactUsed + pure $ CRAcceptingContactRequest user ct + APIRejectContact connReqId -> withUser $ \user -> do + cReq@UserContactRequest {userContactLinkId, agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- withStore $ \db -> getContactRequest db user connReqId `storeFinally` liftIO (deleteContactRequest db user connReqId) - withAgent $ \a -> rejectContact a connId invId - pure $ CRContactRequestRejected user cReq + withUserContactLock "rejectContact" userContactLinkId $ do + withAgent $ \a -> rejectContact a connId invId + pure $ CRContactRequestRejected user cReq APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call ct <- withStore $ \db -> getContact db vr user contactId @@ -1067,7 +1135,7 @@ processChatCommand' vr = \case if featureAllowed SCFCalls forUser ct then do calls <- asks currentCalls - withChatLock "sendCallInvitation" $ do + withContactLock "sendCallInvitation" contactId $ do g <- asks random callId <- atomically $ CallId <$> C.randomBytes 16 g dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing @@ -1090,7 +1158,11 @@ processChatCommand' vr = \case CallInvitationReceived {} -> do let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0 withStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) - updateDirectChatItemView user ct chatItemId aciContent False Nothing $> Nothing + timed_ <- contactCITimed ct + updateDirectChatItemView user ct chatItemId aciContent False False timed_ Nothing + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId) + pure Nothing _ -> throwChatError . CECallState $ callStateTag callState APISendCallOffer contactId WebRTCCallOffer {callType, rtcSession} -> -- party accepting call @@ -1102,7 +1174,7 @@ processChatCommand' vr = \case aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0 (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallOffer callId offer) withStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) - updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId + updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId pure $ Just call {callState = callState'} _ -> throwChatError . CECallState $ callStateTag callState APISendCallAnswer contactId rtcSession -> @@ -1112,7 +1184,7 @@ processChatCommand' vr = \case let callState' = CallNegotiated {localCallType, peerCallType, localCallSession = rtcSession, peerCallSession, sharedKey} aciContent = ACIContent SMDSnd $ CISndCall CISCallNegotiated 0 (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallAnswer callId CallAnswer {rtcSession}) - updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId + updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId pure $ Just call {callState = callState'} _ -> throwChatError . CECallState $ callStateTag callState APISendCallExtraInfo contactId rtcExtraInfo -> @@ -1167,6 +1239,25 @@ processChatCommand' vr = \case conn <- getPendingContactConnection db userId connId liftIO $ updateContactConnectionAlias db userId conn localAlias pure $ CRConnectionAliasUpdated user conn' + APISetUserUIThemes uId uiThemes -> withUser $ \user@User {userId} -> do + user'@User {userId = uId'} <- withStore $ \db -> do + user' <- getUser db uId + liftIO $ setUserUIThemes db user uiThemes + pure user' + when (userId == uId') $ chatWriteVar currentUser $ Just (user :: User) {uiThemes} + ok user' + APISetChatUIThemes (ChatRef cType chatId) uiThemes -> withUser $ \user -> case cType of + CTDirect -> do + withStore $ \db -> do + ct <- getContact db vr user chatId + liftIO $ setContactUIThemes db user ct uiThemes + ok user + CTGroup -> do + withStore $ \db -> do + g <- getGroupInfo db vr user chatId + liftIO $ setGroupUIThemes db user g uiThemes + ok user + _ -> pure $ chatCmdError (Just user) "not supported" APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken APIRegisterToken token mode -> withUser $ \_ -> @@ -1192,12 +1283,11 @@ processChatCommand' vr = \case toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> processChatCommand $ APIGetUserProtoServers userId aProtocol - APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ - withChatLock "setUserSMPServers" $ do - withStore $ \db -> overwriteProtocolServers db user servers - cfg <- asks config - lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers - ok user + APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ do + withStore $ \db -> overwriteProtocolServers db user servers + cfg <- asks config + lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers + ok user SetUserProtoServers serversConfig -> withUser $ \User {userId} -> processChatCommand $ APISetUserProtoServers userId serversConfig APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> @@ -1230,6 +1320,7 @@ processChatCommand' vr = \case APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_ APIGetNetworkConfig -> withUser' $ \_ -> lift $ CRNetworkConfig <$> withAgent' getNetworkConfig + APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_ ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_ APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of CTDirect -> do @@ -1300,7 +1391,7 @@ processChatCommand' vr = \case connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId pure $ CRGroupMemberSwitchAborted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive - APISyncContactRatchet contactId force -> withUser $ \user -> withChatLock "syncContactRatchet" $ do + APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do ct <- withStore $ \db -> getContact db vr user contactId case contactConn ct of Just conn@Connection {pqSupport} -> do @@ -1308,7 +1399,7 @@ processChatCommand' vr = \case createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCERatchetSync rss Nothing) Nothing pure $ CRContactRatchetSyncStarted user ct cStats Nothing -> throwChatError $ CEContactNotActive ct - APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withChatLock "syncGroupMemberRatchet" $ do + APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withGroupLock "syncGroupMemberRatchet" gId $ do (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId case memberConnId m of Just connId -> do @@ -1397,14 +1488,13 @@ processChatCommand' vr = \case EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome - APIAddContact userId incognito -> withUserId userId $ \user -> withChatLock "addContact" . procCmd $ do + APIAddContact userId incognito -> withUserId userId $ \user -> procCmd $ do -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - pqSup <- chatReadVar pqExperimentalEnabled - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing (IKNoPQ pqSup) subMode + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode -- TODO PQ pass minVersion from the current range - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode initialChatVersion pqSup + conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode initialChatVersion PQSupportOn pure $ CRInvitation user cReq conn AddContact incognito -> withUser $ \User {userId} -> processChatCommand $ APIAddContact userId incognito @@ -1424,22 +1514,21 @@ processChatCommand' vr = \case Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' Nothing -> throwChatError CEConnectionIncognitoChangeProhibited APIConnectPlan userId cReqUri -> withUserId userId $ \user -> - withChatLock "connectPlan" . procCmd $ - CRConnectionPlan user <$> connectPlan user cReqUri - APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do + CRConnectionPlan user <$> connectPlan user cReqUri + APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing False - pqSup <- chatReadVar pqExperimentalEnabled - lift (withAgent' $ \a -> connRequestPQSupport a pqSup cReq) >>= \case + lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq -- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan Just (agentV, pqSup') -> do let chatV = agentToChatVersion agentV dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm pqSup' subMode + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup' + void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup' subMode pure $ CRSentConfirmation user conn APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq @@ -1471,10 +1560,9 @@ processChatCommand' vr = \case CRContactsList user <$> withStore' (\db -> getUserContacts db vr user) ListContacts -> withUser $ \User {userId} -> processChatCommand $ APIListContacts userId - APICreateMyAddress userId -> withUserId userId $ \user -> withChatLock "createMyAddress" . procCmd $ do + APICreateMyAddress userId -> withUserId userId $ \user -> procCmd $ do subMode <- chatReadVar subscriptionMode - -- TODO v5.7 pass IPPQOn - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing IKPQOff subMode + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing IKPQOn subMode withStore $ \db -> createUserContactLink db user connId cReq subMode pure $ CRUserContactLinkCreated user cReq CreateMyAddress -> withUser $ \User {userId} -> @@ -1516,6 +1604,21 @@ processChatCommand' vr = \case RejectContact cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIRejectContact connReqId + ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do + contactId <- withStore $ \db -> getContactIdByName db user fromContactName + forwardedItemId <- withStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg + toChatRef <- getChatRef user toChatName + processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId + ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do + groupId <- withStore $ \db -> getGroupIdByName db user fromGroupName + forwardedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg + toChatRef <- getChatRef user toChatName + processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId + ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do + folderId <- withStore (`getUserNoteFolderId` user) + forwardedItemId <- withStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg + toChatRef <- getChatRef user toChatName + processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId SendMessage (ChatName cType name) msg -> withUser $ \user -> do let mc = MCText msg case cType of @@ -1550,8 +1653,9 @@ processChatCommand' vr = \case let mc = MCText msg case memberContactId m of Nothing -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user gId - toView $ CRNoMemberContactCreating user gInfo m + g <- withStore $ \db -> getGroupInfo db vr user gId + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" + toView $ CRNoMemberContactCreating user g m processChatCommand (APICreateMemberContact gId mId) >>= \case cr@(CRNewMemberContact _ Contact {contactId} _ _) -> do toView cr @@ -1587,8 +1691,8 @@ processChatCommand' vr = \case addContactConn ct ctConns = case contactSendConn_ ct of Right conn | directOrUsed ct -> (ct, conn) : ctConns _ -> ctConns - ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, PQSupport, ChatMsgEvent 'Json) - ctSndEvent (_, Connection {connId, pqSupport}) = (ConnectionId connId, pqSupport, XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, ChatMsgEvent 'Json) + ctSndEvent (_, Connection {connId}) = (ConnectionId connId, XMsgNew $ MCSimple (extMsgContent mc Nothing)) ctMsgReq :: (Contact, Connection) -> SndMessage -> MsgReq ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, msgBody, msgId) zipWith3' :: (a -> b -> c -> d) -> NonEmpty a -> NonEmpty b -> NonEmpty c -> NonEmpty d @@ -1599,7 +1703,7 @@ processChatCommand' vr = \case combineResults _ _ (Left e) = Left e createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO () createCI db user createdAt (ct, sndMsg) = - void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing False createdAt + void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do contactId <- withStore $ \db -> getContactIdByName db user cName quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg @@ -1636,7 +1740,7 @@ processChatCommand' vr = \case pure $ CRGroupCreated user groupInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> processChatCommand $ APINewGroup userId incognito gProfile - APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do + APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId assertDirectAllowed user MDSnd contact XGrpInv_ @@ -1666,7 +1770,7 @@ processChatCommand' vr = \case Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do - withChatLock "joinGroup" . procCmd $ do + withGroupLock "joinGroup" groupId . procCmd $ do (invitation, ct) <- withStore $ \db -> do inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId (inv,) <$> getContactViaMember db vr user fromMember @@ -1677,12 +1781,13 @@ processChatCommand' vr = \case Just Connection {peerChatVRange} -> do subMode <- chatReadVar subscriptionMode dm <- encodeConnInfo $ XGrpAcpt membershipMemId - agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm PQSupportOff subMode - let chatV = vr PQSupportOff `peerConnChatVersion` peerChatVRange + agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff + let chatV = vr `peerConnChatVersion` peerChatVRange withStore' $ \db -> do createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted + void . withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure () pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct @@ -1697,7 +1802,7 @@ processChatCommand' vr = \case changeMemberRole user gInfo members m gEvent = do let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m assertUserGroupRole gInfo $ maximum [GRAdmin, mRole, memRole] - withChatLock "memberRole" . procCmd $ do + withGroupLock "memberRole" groupId . procCmd $ do unless (mRole == memRole) $ do withStore' $ \db -> updateGroupMemberRole db user m memRole case mStatus of @@ -1719,7 +1824,7 @@ processChatCommand' vr = \case let GroupMember {memberId = bmMemberId, memberRole = bmRole, memberProfile = bmp} = bm assertUserGroupRole gInfo $ max GRAdmin bmRole when (blocked == blockedByAdmin bm) $ throwChatError $ CECommandError $ if blocked then "already blocked" else "already unblocked" - withChatLock "blockForAll" . procCmd $ do + withGroupLock "blockForAll" groupId . procCmd $ do let mrs = if blocked then MRSBlocked else MRSUnrestricted event = XGrpMemRestrict bmMemberId MemberRestrictions {restriction = mrs} (msg, _) <- sendGroupMessage' user gInfo remainingMembers event @@ -1741,7 +1846,7 @@ processChatCommand' vr = \case Nothing -> throwChatError CEGroupMemberNotFound Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do assertUserGroupRole gInfo $ max GRAdmin mRole - withChatLock "removeMember" . procCmd $ do + withGroupLock "removeMember" groupId . procCmd $ do case mStatus of GSMemInvited -> do deleteMemberConnection user m @@ -1757,7 +1862,7 @@ processChatCommand' vr = \case APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo - withChatLock "leaveGroup" . procCmd $ do + withGroupLock "leaveGroup" groupId . procCmd $ do cancelFilesInProgress user filesInfo (msg, _) <- sendGroupMessage' user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) @@ -1807,7 +1912,7 @@ processChatCommand' vr = \case updateGroupProfileByName gName $ \p -> p {description} ShowGroupDescription gName -> withUser $ \user -> CRGroupDescription user <$> withStore (\db -> getGroupInfoByName db vr user gName) - APICreateGroupLink groupId mRole -> withUser $ \user -> withChatLock "createGroupLink" $ do + APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole @@ -1817,14 +1922,14 @@ processChatCommand' vr = \case (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) IKPQOff subMode withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo cReq mRole - APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withChatLock "groupLinkMemberRole " $ do + APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId (groupLinkId, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo assertUserGroupRole gInfo GRAdmin when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole' when (mRole' /= mRole) $ withStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole' pure $ CRGroupLink user gInfo groupLink mRole' - APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do + APIDeleteGroupLink groupId -> withUser $ \user -> withGroupLock "deleteGroupLink" groupId $ do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId deleteGroupLink' user gInfo pure $ CRGroupLinkDeleted user gInfo @@ -1835,12 +1940,13 @@ processChatCommand' vr = \case APICreateMemberContact gId gMemberId -> withUser $ \user -> do (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId assertUserGroupRole g GRAuthor - unless (groupFeatureAllowed SGFDirectMessages g) $ throwChatError $ CECommandError "direct messages not allowed" + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" case memberConn m of Just mConn@Connection {peerChatVRange} -> do unless (maxVersion peerChatVRange >= groupDirectInvVersion) $ throwChatError CEPeerChatVRangeIncompatible when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists" subMode <- chatReadVar subscriptionMode + -- TODO PQ should negotitate contact connection with PQSupportOn? (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode @@ -1932,19 +2038,19 @@ processChatCommand' vr = \case ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> - withChatLock "receiveFile" . procCmd $ do - (user, ft) <- withStore (`getRcvFileTransferById` fileId) + withFileLock "receiveFile" fileId . procCmd $ do + (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId) encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles - ft' <- (if encrypt then setFileToEncrypt else pure) ft + ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft receiveFile' user ft' rcvInline_ filePath_ SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do - withChatLock "setFileToReceive" . procCmd $ do + withFileLock "setFileToReceive" fileId . procCmd $ do encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing withStore' $ \db -> setRcvFileToReceive db fileId cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> - withChatLock "cancelFile" . procCmd $ + withFileLock "cancelFile" fileId . procCmd $ withStore (\db -> getFileTransfer db user fileId) >>= \case FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" @@ -2016,9 +2122,12 @@ processChatCommand' vr = \case ct@Contact {userPreferences} <- withStore $ \db -> getContactByName db vr user cName let prefs' = setPreference f allowed_ $ Just userPreferences updateContactPrefs user ct prefs' - SetGroupFeature (AGF f) gName enabled -> + SetGroupFeature (AGFNR f) gName enabled -> updateGroupProfileByName gName $ \p -> p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p} + SetGroupFeatureRole (AGFR f) gName enabled role -> + updateGroupProfileByName gName $ \p -> + p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p} SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do let allowed = if onOff then FAYes else FANo pref = TimedMessagesPreference allowed Nothing @@ -2074,8 +2183,19 @@ processChatCommand' vr = \case pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} DebugLocks -> lift $ do chatLockName <- atomically . tryReadTMVar =<< asks chatLock + chatEntityLocks <- getLocks =<< asks entityLocks agentLocks <- withAgent' debugAgentLocks - pure CRDebugLocks {chatLockName, agentLocks} + pure CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} + 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 + DebugEvent event -> toView event >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails GetAgentStats -> lift $ CRAgentStats . map stat <$> withAgent' getAgentStats @@ -2101,7 +2221,6 @@ processChatCommand' vr = \case -- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported" where - withChatLock name action = asks chatLock >>= \l -> withLock l name action -- below code would make command responses asynchronous where they can be slow -- in View.hs `r'` should be defined as `id` in this case -- procCmd :: m ChatResponse -> m ChatResponse @@ -2167,7 +2286,7 @@ processChatCommand' vr = \case CTLocal -> withStore $ \db -> getLocalChatItemIdByText' db user cId msg _ -> throwChatError $ CECommandError "not supported" connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> CM ChatResponse - connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do + connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq case groupLinkId of @@ -2192,24 +2311,29 @@ processChatCommand' vr = \case connect' (Just gLinkId) cReqHash xContactId True where connect' groupLinkId cReqHash xContactId inGroup = do - pqSup <- if inGroup then pure PQSupportOff else chatReadVar pqExperimentalEnabled - (connId, incognitoProfile, subMode, chatV) <- requestContact user incognito cReq xContactId inGroup pqSup + let pqSup = if inGroup then PQSupportOff else PQSupportOn + (connId, chatV) <- prepareContact user cReq pqSup + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + subMode <- chatReadVar subscriptionMode conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup + joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV pure $ CRSentInvitation user conn incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> CM ChatResponse connectContactViaAddress user incognito ct cReq = - withChatLock "connectViaContact" $ do + withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do newXContactId <- XContactId <$> drgRandomBytes 16 - pqSup <- chatReadVar pqExperimentalEnabled - (connId, incognitoProfile, subMode, chatV) <- requestContact user incognito cReq newXContactId False pqSup + let pqSup = PQSupportOn + (connId, chatV) <- prepareContact user cReq pqSup let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + subMode <- chatReadVar subscriptionMode ct' <- withStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup + joinContact user connId cReq incognitoProfile newXContactId False pqSup chatV pure $ CRSentInvitationToContact user ct' incognitoProfile - requestContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> XContactId -> Bool -> PQSupport -> CM (ConnId, Maybe Profile, SubscriptionMode, VersionChat) - requestContact user incognito cReq xContactId inGroup pqSup = do - -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup + prepareContact :: User -> ConnectionRequestUri 'CMContact -> PQSupport -> CM (ConnId, VersionChat) + prepareContact user cReq pqSup = do -- 0) toggle disabled - PQSupportOff -- 1) toggle enabled, address supports PQ (connRequestPQSupport returns Just True) - PQSupportOn, enable support with compression -- 2) toggle enabled, address doesn't support PQ - PQSupportOn but without compression, with version range indicating support @@ -2217,10 +2341,14 @@ processChatCommand' vr = \case Nothing -> throwChatError CEInvalidConnReq Just (agentV, _) -> do let chatV = agentToChatVersion agentV - dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId) - subMode <- chatReadVar subscriptionMode - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm pqSup subMode - pure (connId, incognitoProfile, subMode, chatV) + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup + pure (connId, chatV) + joinContact :: User -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM () + joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV = do + let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup + dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId) + subMode <- chatReadVar subscriptionMode + void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup subMode contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -2265,15 +2393,16 @@ processChatCommand' vr = \case -- [incognito] filter out contacts with whom user has incognito connections addChangedProfileContact :: User -> Contact -> [ChangedProfileContact] -> [ChangedProfileContact] addChangedProfileContact user' ct changedCts = case contactSendConn_ ct' of - Right conn | not (connIncognito conn) && mergedProfile' /= mergedProfile -> - ChangedProfileContact ct ct' mergedProfile' conn : changedCts + Right conn + | not (connIncognito conn) && mergedProfile' /= mergedProfile -> + ChangedProfileContact ct ct' mergedProfile' conn : changedCts _ -> changedCts where mergedProfile = userProfileToSend user Nothing (Just ct) False ct' = updateMergedPreferences user' ct mergedProfile' = userProfileToSend user' Nothing (Just ct') False - ctSndEvent :: ChangedProfileContact -> (ConnOrGroupId, PQSupport, ChatMsgEvent 'Json) - ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId, pqSupport}} = (ConnectionId connId, pqSupport, XInfo mergedProfile') + ctSndEvent :: ChangedProfileContact -> (ConnOrGroupId, ChatMsgEvent 'Json) + ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = (ConnectionId connId, XInfo mergedProfile') ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError MsgReq ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} -> @@ -2289,7 +2418,7 @@ processChatCommand' vr = \case let mergedProfile = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct) False mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False when (mergedProfile' /= mergedProfile) $ - withChatLock "updateProfile" $ do + withContactLock "updateProfile" (contactId' ct) $ do void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` (toView . CRChatError (Just user)) lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' @@ -2334,7 +2463,7 @@ processChatCommand' vr = \case user <- getUserByContactId db ctId (user,) <$> getContact db vr user ctId calls <- asks currentCalls - withChatLock "currentCall" $ + withContactLock "currentCall" ctId $ atomically (TM.lookup ctId calls) >>= \case Nothing -> throwChatError CENoCurrentCall Just call@Call {contactId} @@ -2368,7 +2497,7 @@ processChatCommand' vr = \case groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName pure (groupId, groupMemberId) sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM () - sendGrpInvitation user ct@Contact {localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do + sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo let GroupMember {memberRole = userRole, memberId = userMemberId} = membership groupInv = @@ -2382,19 +2511,11 @@ processChatCommand' vr = \case } (msg, _) <- sendDirectContactMessage user ct $ XGrpInv groupInv let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole - ci <- saveSndChatItem user (CDDirectSnd ct) msg content + timed_ <- contactCITimed ct + ci <- saveSndChatItem' user (CDDirectSnd ct) msg content Nothing Nothing Nothing timed_ False toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) - sndContactCITimed :: Bool -> Contact -> Maybe Int -> CM (Maybe CITimed) - sndContactCITimed live = sndCITimed_ live . contactTimedTTL - sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> CM (Maybe CITimed) - sndGroupCITimed live = sndCITimed_ live . groupTimedTTL - sndCITimed_ :: Bool -> Maybe (Maybe Int) -> Maybe Int -> CM (Maybe CITimed) - sndCITimed_ live chatTTL itemTTL = - forM (chatTTL >>= (itemTTL <|>)) $ \ttl -> - CITimed ttl - <$> if live - then pure Nothing - else Just . addUTCTime (realToFrac ttl) <$> liftIO getCurrentTime + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) drgRandomBytes :: Int -> CM ByteString drgRandomBytes n = asks random >>= atomically . C.randomBytes n privateGetUser :: UserId -> CM User @@ -2530,11 +2651,129 @@ processChatCommand' vr = \case updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId case (cInfo, content) of - (DirectChat ct, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole) + (DirectChat ct@Contact {contactId}, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole) | status == CIGISPending -> do let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = newStatus} memRole - updateDirectChatItemView user ct itemId aciContent False Nothing + timed_ <- contactCITimed ct + updateDirectChatItemView user ct itemId aciContent False False timed_ Nothing + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, itemId) _ -> pure () -- prohibited + sendContactContentMessage :: User -> ContactId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse + sendContactContentMessage user contactId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do + ct@Contact {contactUsed} <- withStore $ \db -> getContact db vr user contactId + assertDirectAllowed user MDSnd ct XMsgNew_ + unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct + if isVoice mc && not (featureAllowed SCFVoice forUser ct) + then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) + else do + (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer ct + timed_ <- sndContactCITimed live ct itemTTL + (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ + (msg, _) <- sendDirectContactMessage user ct (XMsgNew msgContainer) + ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) + pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + where + setupSndFileTransfer :: Contact -> CM (Maybe (FileInvitation, CIFile 'MDSnd)) + setupSndFileTransfer ct = forM file_ $ \file -> do + fileSize <- checkSndFile file + xftpSndFileTransfer user file fileSize 1 $ CGContact ct + prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> CM (MsgContainer, Maybe (CIQuote 'CTDirect)) + prepareMsg fInv_ timed_ = case (quotedItemId_, itemForwarded) of + (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Just quotedItemId, Nothing) -> do + CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- + withStore $ \db -> getDirectChatItem db user contactId quotedItemId + (origQmc, qd, sent) <- quoteData qci + let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} + qmc = quoteContent mc origQmc file + quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} + pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) + (Just _, Just _) -> throwChatError CEInvalidQuote + where + quoteData :: ChatItem c d -> CM (MsgContent, CIQDirection 'CTDirect, Bool) + quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote + quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) + quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) + quoteData _ = throwChatError CEInvalidQuote + sendGroupContentMessage :: User -> GroupId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse + sendGroupContentMessage user groupId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do + g@(Group gInfo _) <- withStore $ \db -> getGroup db vr user groupId + assertUserGroupRole gInfo GRAuthor + send g + where + send g@(Group gInfo@GroupInfo {membership} ms) = + case prohibitedGroupContent gInfo membership mc file_ of + Just f -> notAllowedError f + Nothing -> do + (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms) + timed_ <- sndGroupCITimed live gInfo itemTTL + (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ itemForwarded fInv_ timed_ live + (msg, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) + ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live + withStore' $ \db -> + forM_ sentToMembers $ \GroupMember {groupMemberId} -> + createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) + pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f)) + setupSndFileTransfer :: Group -> Int -> CM (Maybe (FileInvitation, CIFile 'MDSnd)) + setupSndFileTransfer g n = forM file_ $ \file -> do + fileSize <- checkSndFile file + xftpSndFileTransfer user file fileSize n $ CGGroup g + xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd) + xftpSndFileTransfer user file fileSize n contactOrGroup = do + (fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup + case contactOrGroup of + CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> + withStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr + CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user)) + where + -- we are not sending files to pending members, same as with inline files + saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = + when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $ + withStore' $ + \db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr + saveMemberFD _ = pure () + pure (fInv, ciFile) + createNoteFolderContentItem :: User -> NoteFolderId -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse + createNoteFolderContentItem user folderId (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do + forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported" + nf <- withStore $ \db -> getNoteFolder db user folderId + createdAt <- liftIO getCurrentTime + let content = CISndMsgContent mc + let cd = CDLocalSnd nf + ciId <- createLocalChatItem user cd content itemForwarded createdAt + ciFile_ <- forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do + fsFilePath <- lift $ toFSFilePath filePath + fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs + chunkSize <- asks $ fileChunkSize . config + withStore' $ \db -> do + fileId <- createLocalFile CIFSSndStored db user nf ciId createdAt cf fileSize chunkSize + pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} + let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt + pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci + +contactCITimed :: Contact -> CM (Maybe CITimed) +contactCITimed ct = sndContactCITimed False ct Nothing + +sndContactCITimed :: Bool -> Contact -> Maybe Int -> CM (Maybe CITimed) +sndContactCITimed live = sndCITimed_ live . contactTimedTTL + +sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> CM (Maybe CITimed) +sndGroupCITimed live = sndCITimed_ live . groupTimedTTL + +sndCITimed_ :: Bool -> Maybe (Maybe Int) -> Maybe Int -> CM (Maybe CITimed) +sndCITimed_ live chatTTL itemTTL = + forM (chatTTL >>= (itemTTL <|>)) $ \ttl -> + CITimed ttl + <$> if live + then pure Nothing + else Just . addUTCTime (realToFrac ttl) <$> liftIO getCurrentTime toggleNtf :: User -> GroupMember -> Bool -> CM () toggleNtf user m ntfOn = @@ -2549,10 +2788,11 @@ data ChangedProfileContact = ChangedProfileContact conn :: Connection } -prepareGroupMsg :: User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe FileInvitation -> Maybe CITimed -> Bool -> CM (MsgContainer, Maybe (CIQuote 'CTGroup)) -prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ timed_ live = case quotedItemId_ of - Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) - Just quotedItemId -> do +prepareGroupMsg :: User -> GroupInfo -> MsgContent -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> CM (MsgContainer, Maybe (CIQuote 'CTGroup)) +prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of + (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Just quotedItemId, Nothing) -> do CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- withStore $ \db -> getGroupChatItem db user groupId quotedItemId (origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership @@ -2560,6 +2800,7 @@ prepareGroupMsg user GroupInfo {groupId, membership} mc quotedItemId_ fInv_ time qmc = quoteContent mc origQmc file quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) + (Just _, Just _) -> throwChatError CEInvalidQuote where quoteData :: ChatItem c d -> GroupMember -> CM (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember) quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote @@ -2598,7 +2839,7 @@ assertDirectAllowed user dir ct event = unless (allowedChatEvent || anyDirectOrUsed ct) . unlessM directMessagesAllowed $ throwChatError (CEDirectMessagesProhibited dir ct) where - directMessagesAllowed = any (groupFeatureAllowed' SGFDirectMessages) <$> withStore' (\db -> getContactGroupPreferences db user ct) + directMessagesAllowed = any (uncurry $ groupFeatureMemberAllowed' SGFDirectMessages) <$> withStore' (\db -> getContactGroupPreferences db user ct) allowedChatEvent = case event of XMsgNew_ -> False XMsgUpdate_ -> False @@ -2608,6 +2849,13 @@ assertDirectAllowed user dir ct event = XCallInv_ -> False _ -> True +prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe f -> Maybe GroupFeature +prohibitedGroupContent gInfo m mc file_ + | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice + | not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles + | not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) && containsFormat isSimplexLink (parseMarkdown $ msgContentText mc) = Just GFSimplexLinks + | otherwise = Nothing + roundedFDCount :: Int -> Int roundedFDCount n | n <= 0 = 4 @@ -2714,13 +2962,30 @@ deleteFilesLocally files = withFilesFolder action = asks filesFolder >>= readTVarIO >>= mapM_ action updateCallItemStatus :: User -> Contact -> Call -> WebRTCCallStatus -> Maybe MessageId -> CM () -updateCallItemStatus user ct Call {chatItemId} receivedStatus msgId_ = do +updateCallItemStatus user ct@Contact {contactId} Call {chatItemId} receivedStatus msgId_ = do aciContent_ <- callStatusItemContent user ct chatItemId receivedStatus - forM_ aciContent_ $ \aciContent -> updateDirectChatItemView user ct chatItemId aciContent False msgId_ + forM_ aciContent_ $ \aciContent -> do + timed_ <- callTimed ct aciContent + updateDirectChatItemView user ct chatItemId aciContent False False timed_ msgId_ + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId) -updateDirectChatItemView :: User -> Contact -> ChatItemId -> ACIContent -> Bool -> Maybe MessageId -> CM () -updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) live msgId_ = do - ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent live msgId_ +callTimed :: Contact -> ACIContent -> CM (Maybe CITimed) +callTimed ct aciContent = + case aciContentCallStatus aciContent of + Just callStatus + | callComplete callStatus -> do + contactCITimed ct + _ -> pure Nothing + where + aciContentCallStatus :: ACIContent -> Maybe CICallStatus + aciContentCallStatus (ACIContent _ (CISndCall st _)) = Just st + aciContentCallStatus (ACIContent _ (CIRcvCall st _)) = Just st + aciContentCallStatus _ = Nothing + +updateDirectChatItemView :: User -> Contact -> ChatItemId -> ACIContent -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> CM () +updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) edited live timed_ msgId_ = do + ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent edited live timed_ msgId_ toView $ CRChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci') callStatusItemContent :: User -> Contact -> ChatItemId -> WebRTCCallStatus -> CM (Maybe ACIContent) @@ -2911,10 +3176,10 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of acceptContactRequest :: User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> CM Contact acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId, pqSupport} incognitoProfile contactUsed = do subMode <- chatReadVar subscriptionMode - pqSup <- chatReadVar pqExperimentalEnabled + let pqSup = PQSupportOn vr <- chatVersionRange let profileToSend = profileToSendOnAccept user incognitoProfile False - chatV = vr pqSup `peerConnChatVersion` cReqChatVRange + chatV = vr `peerConnChatVersion` cReqChatVRange pqSup' = pqSup `CR.pqSupportAnd` pqSupport dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend acId <- withAgent $ \a -> acceptContact a True invId dm pqSup' subMode @@ -2925,7 +3190,7 @@ acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvI subMode <- chatReadVar subscriptionMode let profileToSend = profileToSendOnAccept user incognitoProfile False vr <- chatVersionRange - chatV <- (\pq -> vr pq `peerConnChatVersion` cReqChatVRange) <$> chatReadVar pqExperimentalEnabled + let chatV = vr `peerConnChatVersion` cReqChatVRange (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV withStore' $ \db -> do ct@Contact {activeConn} <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed @@ -2955,7 +3220,7 @@ acceptGroupJoinRequestAsync } subMode <- chatReadVar subscriptionMode vr <- chatVersionRange - chatV <- (\pq -> vr pq `peerConnChatVersion` cReqChatVRange) <$> chatReadVar pqExperimentalEnabled + let chatV = vr `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV withStore $ \db -> do liftIO $ createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode @@ -2988,25 +3253,20 @@ deleteGroupLink_ user gInfo conn = do agentSubscriber :: CM' () agentSubscriber = do q <- asks $ subQ . smpAgent - l <- asks chatLock - forever $ atomically (readTBQueue q) >>= process l + forever $ atomically (readTBQueue q) >>= process where - process :: Lock -> (ACorrId, EntityId, APartyCmd 'Agent) -> CM' () - process l (corrId, entId, APC e msg) = run $ case e of + process :: (ACorrId, EntityId, APartyCmd 'Agent) -> CM' () + process (corrId, entId, APC e msg) = run $ case e of SAENone -> processAgentMessageNoConn msg SAEConn -> processAgentMessage corrId entId msg SAERcvFile -> processAgentMsgRcvFile corrId entId msg SAESndFile -> processAgentMsgSndFile corrId entId msg where - run action = do - let name = "agentSubscriber entity=" <> show e <> " entId=" <> str entId <> " msg=" <> str (aCommandTag msg) - withLock' l name $ action `catchChatError'` (toView' . CRChatError Nothing) - str :: StrEncoding a => a -> String - str = B.unpack . strEncode + run action = action `catchChatError'` (toView' . CRChatError Nothing) type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ())) -subscribeUserConnections :: (PQSupport -> VersionRangeChat) -> Bool -> AgentBatchSubscribe -> User -> CM () +subscribeUserConnections :: VersionRangeChat -> Bool -> AgentBatchSubscribe -> User -> CM () subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do -- get user connections ce <- asks $ subscriptionEvents . config @@ -3150,8 +3410,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do forM_ err_ $ toView . CRSndFileSubError user ft void . forkIO $ do threadDelay 1000000 - l <- asks chatLock - when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withLock l "subscribe sendFileChunk" $ + when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $ sendFileChunk user ft rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM () rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs @@ -3301,7 +3560,7 @@ expireChatItems user@User {userId} ttl sync = do cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo withStore' $ \db -> deleteContactExpiredCIs db user ct expirationDate - processGroup :: (PQSupport -> VersionRangeChat) -> UTCTime -> UTCTime -> GroupInfo -> CM () + processGroup :: VersionRangeChat -> UTCTime -> UTCTime -> GroupInfo -> CM () processGroup vr expirationDate createdAtCutoff gInfo = do lift waitChatStartedAndActivated filesInfo <- withStore' $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff @@ -3316,12 +3575,16 @@ processAgentMessage _ connId (DEL_RCVQ srv qId err_) = toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_ processAgentMessage _ connId DEL_CONN = toView $ CRAgentConnDeleted (AgentConnId connId) +processAgentMessage _ "" (ERR e) = + toView $ CRChatError Nothing $ ChatErrorAgent e Nothing processAgentMessage corrId connId msg = do - vr <- chatVersionRange - -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here - critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case - Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) - _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) + lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId)) + withEntityLock "processAgentMessage" lockEntity $ do + vr <- chatVersionRange + -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here + critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case + Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) + _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. -- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries, @@ -3358,18 +3621,23 @@ processAgentMessageNoConn = \case toView $ event srv cs processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM () -processAgentMsgSndFile _corrId aFileId msg = - withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case - Just user -> process user `catchChatError` (toView . CRChatError (Just user)) - _ -> do - lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) - throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId +processAgentMsgSndFile _corrId aFileId msg = do + (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) + withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $ + withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case + Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) + _ -> do + lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) + throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId where - process :: User -> CM () - process user = do - (ft@FileTransferMeta {fileId, xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> do - fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId - getSndFileTransfer db user fileId + withEntityLock_ :: Maybe ChatRef -> CM a -> CM a + withEntityLock_ cRef_ = case cRef_ of + Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgSndFile" contactId + Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgSndFile" groupId + _ -> id + process :: User -> FileTransferId -> CM () + process user fileId = do + (ft@FileTransferMeta {xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId vr <- chatVersionRange unless cancelled $ case msg of SFPROG sndProgress sndTotal -> do @@ -3386,11 +3654,11 @@ processAgentMsgSndFile _corrId aFileId msg = lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds) case rfds of - [] -> sendFileError "no receiver descriptions" fileId vr ft + [] -> sendFileError "no receiver descriptions" vr ft rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of [] -> case xftpRedirectFor of Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft - Just _ -> sendFileError "Prohibit chaining redirects" fileId vr ft + Just _ -> sendFileError "Prohibit chaining redirects" vr ft rfds' -> do -- we have 1 chunk - use it as URI whether it is redirect or not ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor @@ -3439,7 +3707,7 @@ processAgentMsgSndFile _corrId aFileId msg = | temporaryAgentError e -> throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e | otherwise -> - sendFileError (tshow e) fileId vr ft + sendFileError (tshow e) vr ft where fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text fileDescrText = safeDecodeUtf8 . strEncode @@ -3457,8 +3725,8 @@ processAgentMsgSndFile _corrId aFileId msg = case L.nonEmpty fds of Just fds' -> loopSend fds' Nothing -> pure msgDeliveryId - sendFileError :: Text -> Int64 -> (PQSupport -> VersionRangeChat) -> FileTransferMeta -> CM () - sendFileError err fileId vr ft = do + sendFileError :: Text -> VersionRangeChat -> FileTransferMeta -> CM () + sendFileError err vr ft = do logError $ "Sent file error: " <> err ci <- withStore $ \db -> do liftIO $ updateFileCancelled db user fileId CIFSSndError @@ -3480,18 +3748,23 @@ splitFileDescr rfdText = do else fileDescr <| splitParts (partNo + 1) partSize rest processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM () -processAgentMsgRcvFile _corrId aFileId msg = - withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case - Just user -> process user `catchChatError` (toView . CRChatError (Just user)) - _ -> do - lift $ withAgent' (`xftpDeleteRcvFile` aFileId) - throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId +processAgentMsgRcvFile _corrId aFileId msg = do + (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) + withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $ + withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case + Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) + _ -> do + lift $ withAgent' (`xftpDeleteRcvFile` aFileId) + throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId where - process :: User -> CM () - process user = do - ft@RcvFileTransfer {fileId} <- withStore $ \db -> do - fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId - getRcvFileTransfer db user fileId + withEntityLock_ :: Maybe ChatRef -> CM a -> CM a + withEntityLock_ cRef_ = case cRef_ of + Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgRcvFile" contactId + Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgRcvFile" groupId + _ -> id + process :: User -> FileTransferId -> CM () + process user fileId = do + ft <- withStore $ \db -> getRcvFileTransfer db user fileId vr <- chatVersionRange unless (rcvFileCompleteOrCancelled ft) $ case msg of RFPROG rcvProgress rcvTotal -> do @@ -3523,7 +3796,7 @@ processAgentMsgRcvFile _corrId aFileId msg = agentXFTPDeleteRcvFile aFileId fileId toView $ CRRcvFileError user ci e ft -processAgentMessageConn :: (PQSupport -> VersionRangeChat) -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM () +processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM () processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, -- as in this case no need to ACK message - we can't process messages for this connection anyway. @@ -3597,7 +3870,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- probably this branch is never executed, so there should be no reason -- to save message if contact hasn't been created yet - chat item isn't created anyway withAckMessage' agentConnId meta $ - void $ saveDirectRcvMSG conn meta msgBody + void $ + saveDirectRcvMSG conn meta msgBody SENT msgId -> sentMsgDeliveryEvent conn msgId OK -> @@ -3634,7 +3908,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody let ct'' = ct' {activeConn = Just conn''} :: Contact assertDirectAllowed user MDRcv ct'' $ toCMEventTag event - updateChatLock "direct message" event case event of XMsgNew mc -> newContentMessage ct'' mc msg msgMeta XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr @@ -3757,7 +4030,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci_ <- withStore $ \db -> getDirectChatItemLast db user contactId >>= liftIO - . mapM (\(ci, content') -> updateDirectChatItem' db user contactId ci content' False Nothing) + . mapM (\(ci, content') -> updateDirectChatItem' db user contactId ci content' False False Nothing Nothing) . mdeUpdatedCI e case ci_ of Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) @@ -3904,7 +4177,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = profileToSend = profileToSendOnAccept user profileMode True void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend) groupId sendIntroductions members = do - intros <- withStore' $ \db -> createIntroductions db (maxVersion $ vr PQSupportOff) members m + intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m shuffledIntros <- liftIO $ shuffleIntros intros if m `supportsVersion` batchSendVersion then do @@ -3994,7 +4267,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta quotedItemId_ = quoteItemId =<< quotedItem fInv_ = fst <$> fInvDescr_ - (msgContainer, _) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ itemTimed False + (msgContainer, _) <- prepareGroupMsg user gInfo mc quotedItemId_ Nothing fInv_ itemTimed False let senderVRange = memberChatVRange' sender xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent = XMsgNew msgContainer} fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of @@ -4041,19 +4314,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Right (ACMsg _ chatMsg) -> processEvent chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e Left e -> toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) + forwardMsg_ `catchChatError` \_ -> pure () checkSendRcpt $ rights aChatMsgs - -- currently only a single message is forwarded - let GroupMember {memberRole = membershipMemRole} = membership - when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ case aChatMsgs of - [Right (ACMsg _ chatMsg)] -> forwardMsg_ chatMsg - _ -> pure () where aChatMsgs = parseChatMessages msgBody brokerTs = metaBrokerTs msgMeta processEvent :: MsgEncodingI e => ChatMessage e -> CM () processEvent chatMsg = do (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg - updateChatLock "groupMessage" event case event of XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr @@ -4095,22 +4363,27 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = hasDeliveryReceipt (toCMEventTag chatMsgEvent) - forwardMsg_ :: MsgEncodingI e => ChatMessage e -> CM () - forwardMsg_ chatMsg = - forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do - ChatConfig {highlyAvailable} <- asks config - -- members introduced to this invited member - introducedMembers <- - if memberCategory m == GCInviteeMember - then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable - else pure [] - -- invited members to which this member was introduced - invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable - let GroupMember {memberId} = m - ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) chatMsg' - msg = XGrpMsgForward memberId chatMsg' brokerTs - unless (null ms) . void $ - sendGroupMessage' user gInfo ms msg + forwardMsg_ :: CM () + forwardMsg_ = do + let GroupMember {memberRole = membershipMemRole} = membership + when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ case aChatMsgs of + -- currently only a single message is forwarded + [Right (ACMsg _ chatMsg)] -> + forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do + ChatConfig {highlyAvailable} <- asks config + -- members introduced to this invited member + introducedMembers <- + if memberCategory m == GCInviteeMember + then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable + else pure [] + -- invited members to which this member was introduced + invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable + let GroupMember {memberId} = m + ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) chatMsg' + msg = XGrpMsgForward memberId chatMsg' brokerTs + unless (null ms) . void $ + sendGroupMessage' user gInfo ms msg + _ -> pure () RCVD msgMeta msgRcpt -> withAckMessage' agentConnId msgMeta $ groupMsgReceived gInfo m conn msgMeta msgRcpt @@ -4140,7 +4413,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci_ <- withStore $ \db -> getGroupMemberChatItemLast db user groupId (groupMemberId' m) >>= liftIO - . mapM (\(ci, content') -> updateGroupChatItem db user groupId ci content' False Nothing) + . mapM (\(ci, content') -> updateGroupChatItem db user groupId ci content' False False Nothing) . mdeUpdatedCI e case ci_ of Just ci -> toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) @@ -4357,9 +4630,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Nothing -> do -- [incognito] generate profile to send, create connection with incognito profile incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - pqSup <- chatReadVar pqExperimentalEnabled - let pqSup' = pqSup `CR.pqSupportAnd` reqPQSup - ct <- acceptContactRequestAsync user cReq incognitoProfile True pqSup' + ct <- acceptContactRequestAsync user cReq incognitoProfile True reqPQSup toView $ CRAcceptingContactRequest user ct Just groupId -> do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId @@ -4389,13 +4660,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CRConnectionDisabled connEntity _ -> pure () - updateChatLock :: MsgEncodingI enc => String -> ChatMsgEvent enc -> CM () - updateChatLock name event = do - l <- asks chatLock - atomically $ tryReadTMVar l >>= mapM_ (swapTMVar l . (<> s)) - where - s = " " <> name <> "=" <> B.unpack (strEncode $ toCMEventTag event) - -- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections? -- we could save command records only for agent APIs we process continuations for (INV) withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM () @@ -4433,9 +4697,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- This prevents losing the message that failed to be processed. Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing Left e -> ackMsg msgMeta Nothing >> throwError e - where - ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () - ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt + where + ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () + ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt sentMsgDeliveryEvent :: Connection -> AgentMsgId -> CM () sentMsgDeliveryEvent Connection {connId} msgId = @@ -4613,7 +4877,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs content Nothing timed_ live ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc - updateDirectChatItem' db user contactId ci content live Nothing + updateDirectChatItem' db user contactId ci content True live Nothing Nothing toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') where brokerTs = metaBrokerTs msgMeta @@ -4622,18 +4886,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateRcvChatItem = do cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId case cci of - CChatItem SMDRcv ci@ChatItem {meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} -> do - let changed = mc /= oldMC - if changed || fromMaybe False itemLive - then do - ci' <- withStore' $ \db -> do - when changed $ - addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) - reactions <- getDirectCIReactions db ct sharedMsgId - updateDirectChatItem' db user contactId ci {reactions} content live $ Just msgId - toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') - startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' - else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + CChatItem SMDRcv ci@ChatItem {meta = CIMeta {itemForwarded, itemLive}, content = CIRcvMsgContent oldMC} + | isNothing itemForwarded -> do + let changed = mc /= oldMC + if changed || fromMaybe False itemLive + then do + ci' <- withStore' $ \db -> do + when changed $ + addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) + reactions <- getDirectCIReactions db ct sharedMsgId + let edited = itemLive /= Just True + updateDirectChatItem' db user contactId ci {reactions} content edited live Nothing $ Just msgId + toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') + startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) _ -> messageError "x.msg.update: contact attempted invalid message update" messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> CM () @@ -4704,14 +4970,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> CM () newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded | blockedByAdmin m = createBlockedByAdmin - | isVoice content && not (groupFeatureAllowed SGFVoice gInfo) = rejected GFVoice - | not (isVoice content) && isJust fInv_ && not (groupFeatureAllowed SGFFiles gInfo) = rejected GFFiles - | otherwise = - withStore' (\db -> getCIModeration db vr user gInfo memberId sharedMsgId_) >>= \case - Just ciModeration -> do - applyModeration ciModeration - withStore' $ \db -> deleteCIModeration db gInfo memberId sharedMsgId_ - Nothing -> createContentItem + | otherwise = case prohibitedGroupContent gInfo m content fInv_ of + Just f -> rejected f + Nothing -> + withStore' (\db -> getCIModeration db vr user gInfo memberId sharedMsgId_) >>= \case + Just ciModeration -> do + applyModeration ciModeration + withStore' $ \db -> deleteCIModeration db gInfo memberId sharedMsgId_ + Nothing -> createContentItem where rejected f = void $ newChatItem (CIRcvGroupFeatureRejected f) Nothing Nothing False timed' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo itemTTL @@ -4762,7 +5028,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc - ci' <- updateGroupChatItem db user groupId ci content live Nothing + ci' <- updateGroupChatItem db user groupId ci content True live Nothing blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') where @@ -4781,7 +5047,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when changed $ addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) reactions <- getGroupCIReactions db gInfo memberId sharedMsgId - updateGroupChatItem db user groupId ci {reactions} content live $ Just msgId + let edited = itemLive /= Just True + updateGroupChatItem db user groupId ci {reactions} content edited live $ Just msgId toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' else toView $ CRChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) @@ -5154,8 +5421,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createGroupFeatureItems g@GroupInfo {fullGroupPreferences} m = forM_ allGroupFeatures $ \(AGF f) -> do let p = getGroupPreference f fullGroupPreferences - (_, param) = groupFeatureState p - createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param) Nothing + (_, param, role) = groupFeatureState p + createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param role) Nothing xInfoProbe :: ContactOrMember -> Probe -> CM () xInfoProbe cgm2 probe = do @@ -5327,8 +5594,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> do withStore' $ \db -> deleteCalls db user ctId' atomically $ TM.delete ctId' calls - forM_ aciContent_ $ \aciContent -> - updateDirectChatItemView user ct chatItemId aciContent False $ Just msgId + forM_ aciContent_ $ \aciContent -> do + timed_ <- callTimed ct aciContent + updateDirectChatItemView user ct chatItemId aciContent False False timed_ $ Just msgId + forM_ (timed_ >>= timedDeleteAt') $ + startProximateTimedItemThread user (ChatRef CTDirect ctId', chatItemId) msgCallStateError :: Text -> Call -> CM () msgCallStateError eventName Call {callState} = @@ -5450,8 +5720,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | maxVersion mcvr >= groupDirectInvVersion -> pure Nothing | otherwise -> Just <$> createConn subMode let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo - vr' = vr PQSupportOff - chatV = maybe (minVersion vr') (\peerVR -> vr' `peerConnChatVersion` fromChatVRange peerVR) memChatVRange + chatV = maybe (minVersion vr) (\peerVR -> vr `peerConnChatVersion` fromChatVRange peerVR) memChatVRange void $ withStore $ \db -> createIntroReMember db user gInfo m chatV memInfo memRestrictions groupConnIds directConnIds customUserProfileId subMode _ -> messageError "x.grp.mem.intro can be only sent by host member" where @@ -5499,7 +5768,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user True dcr dm subMode let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo mcvr = maybe chatInitialVRange fromChatVRange memChatVRange - chatV = vr PQSupportOff `peerConnChatVersion` mcvr + chatV = vr `peerConnChatVersion` mcvr withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM () @@ -5666,7 +5935,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> CM () xGrpDirectInv g m mConn connReq mContent_ msg brokerTs = do - unless (groupFeatureAllowed SGFDirectMessages g) $ messageError "x.grp.direct.inv: direct messages not allowed" + unless (groupFeatureMemberAllowed SGFDirectMessages m g) $ messageError "x.grp.direct.inv: direct messages not allowed" let GroupMember {memberContactId} = m subMode <- chatReadVar subscriptionMode case memberContactId of @@ -5698,6 +5967,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = joinConn subMode = do -- [incognito] send membership incognito profile let p = userProfileToSend user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing False + -- TODO PQ should negotitate contact connection with PQSupportOn? (use encodeConnInfoPQ) dm <- encodeConnInfo $ XInfo p joinAgentConnectionAsync user True connReq dm subMode createItems mCt' m' = do @@ -5854,19 +6124,25 @@ metaBrokerTs MsgMeta {broker = (_, brokerTs)} = brokerTs sameMemberId :: MemberId -> GroupMember -> Bool sameMemberId memId GroupMember {memberId} = memId == memberId --- TODO v5.7 for contacts only version upgrade should trigger enabling PQ support/encryption updatePeerChatVRange :: Connection -> VersionRangeChat -> CM Connection -updatePeerChatVRange conn@Connection {connId, connChatVersion = v, peerChatVRange, pqSupport} msgVRange = do - v' <- lift $ upgradedConnVersion pqSupport v msgVRange - if msgVRange /= peerChatVRange || v' /= v +updatePeerChatVRange conn@Connection {connId, connChatVersion = v, peerChatVRange, connType, pqSupport, pqEncryption} msgVRange = do + v' <- lift $ upgradedConnVersion v msgVRange + conn' <- + if msgVRange /= peerChatVRange || v' /= v + then do + withStore' $ \db -> setPeerChatVRange db connId v' msgVRange + pure conn {connChatVersion = v', peerChatVRange = msgVRange} + else pure conn + -- TODO v6.0 remove/review: for contacts only version upgrade should trigger enabling PQ support/encryption + if connType == ConnContact && v' >= pqEncryptionCompressionVersion && (pqSupport /= PQSupportOn || pqEncryption /= PQEncOn) then do - withStore' $ \db -> setPeerChatVRange db connId v' msgVRange - pure conn {connChatVersion = v', peerChatVRange = msgVRange} - else pure conn + withStore' $ \db -> updateConnSupportPQ db connId PQSupportOn PQEncOn + pure conn' {pqSupport = PQSupportOn, pqEncryption = PQEncOn} + else pure conn' updateMemberChatVRange :: GroupMember -> Connection -> VersionRangeChat -> CM (GroupMember, Connection) updateMemberChatVRange mem@GroupMember {groupMemberId} conn@Connection {connId, connChatVersion = v, peerChatVRange} msgVRange = do - v' <- lift $ upgradedConnVersion PQSupportOff v msgVRange + v' <- lift $ upgradedConnVersion v msgVRange if msgVRange /= peerChatVRange || v' /= v then do withStore' $ \db -> do @@ -5876,11 +6152,11 @@ updateMemberChatVRange mem@GroupMember {groupMemberId} conn@Connection {connId, pure (mem {memberChatVRange = msgVRange, activeConn = Just conn'}, conn') else pure (mem, conn) -upgradedConnVersion :: PQSupport -> VersionChat -> VersionRangeChat -> CM' VersionChat -upgradedConnVersion pqSup v peerVR = do +upgradedConnVersion :: VersionChat -> VersionRangeChat -> CM' VersionChat +upgradedConnVersion v peerVR = do vr <- chatVersionRange' -- don't allow reducing agreed connection version - pure $ maybe v (\(Compatible v') -> max v v') $ vr pqSup `compatibleVersion` peerVR + pure $ maybe v (\(Compatible v') -> max v v') $ vr `compatibleVersion` peerVR parseFileDescription :: FilePartyI p => Text -> CM (ValidFileDescription p) parseFileDescription = @@ -6047,7 +6323,7 @@ cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, age Just _ -> do vr <- chatVersionRange (sharedMsgId, conn) <- withStore $ \db -> (,) <$> getSharedMsgIdByFileId db userId fileId <*> getConnectionById db vr user connId - void $ sendDirectMessage_ conn PQSupportOff (BFileChunk sharedMsgId FileChunkCancel) (ConnectionId connId) + void $ sendDirectMessage_ conn (BFileChunk sharedMsgId FileChunkCancel) (ConnectionId connId) _ -> withAgent $ \a -> void . sendMessage a acId PQEncOff SMP.noMsgFlags $ smpEncode FileChunkCancel pure fileConnId fileConnId = if isNothing fileInline then Just acId else Nothing @@ -6087,8 +6363,8 @@ deleteOrUpdateMemberRecord user@User {userId} member = sendDirectContactMessage :: MsgEncodingI e => User -> Contact -> ChatMsgEvent e -> CM (SndMessage, Int64) sendDirectContactMessage user ct chatMsgEvent = do - conn@Connection {connId, pqSupport} <- liftEither $ contactSendConn_ ct - r <- sendDirectMessage_ conn pqSupport chatMsgEvent (ConnectionId connId) + conn@Connection {connId} <- liftEither $ contactSendConn_ ct + r <- sendDirectMessage_ conn chatMsgEvent (ConnectionId connId) let (sndMessage, msgDeliveryId, pqEnc') = r void $ createContactPQSndItem user ct conn pqEnc' pure (sndMessage, msgDeliveryId) @@ -6107,37 +6383,37 @@ contactSendConn_ ct@Contact {activeConn} = case activeConn of -- unlike sendGroupMemberMessage, this function will not store message as pending -- TODO v5.8 we could remove pending messages once all clients support forwarding sendDirectMemberMessage :: MsgEncodingI e => Connection -> ChatMsgEvent e -> GroupId -> CM (SndMessage, Int64, PQEncryption) -sendDirectMemberMessage conn chatMsgEvent groupId = sendDirectMessage_ conn PQSupportOff chatMsgEvent (GroupId groupId) +sendDirectMemberMessage conn chatMsgEvent groupId = sendDirectMessage_ conn chatMsgEvent (GroupId groupId) -sendDirectMessage_ :: MsgEncodingI e => Connection -> PQSupport -> ChatMsgEvent e -> ConnOrGroupId -> CM (SndMessage, Int64, PQEncryption) -sendDirectMessage_ conn pqSup chatMsgEvent connOrGroupId = do +sendDirectMessage_ :: MsgEncodingI e => Connection -> ChatMsgEvent e -> ConnOrGroupId -> CM (SndMessage, Int64, PQEncryption) +sendDirectMessage_ conn chatMsgEvent connOrGroupId = do when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) - msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId pqSup + msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId -- TODO move compressed body to SndMessage and compress in createSndMessage (msgDeliveryId, pqEnc') <- deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId pure (msg, msgDeliveryId, pqEnc') -createSndMessage :: MsgEncodingI e => ChatMsgEvent e -> ConnOrGroupId -> PQSupport -> CM SndMessage -createSndMessage chatMsgEvent connOrGroupId pqSup = - liftEither . runIdentity =<< lift (createSndMessages $ Identity (connOrGroupId, pqSup, chatMsgEvent)) +createSndMessage :: MsgEncodingI e => ChatMsgEvent e -> ConnOrGroupId -> CM SndMessage +createSndMessage chatMsgEvent connOrGroupId = + liftEither . runIdentity =<< lift (createSndMessages $ Identity (connOrGroupId, chatMsgEvent)) -createSndMessages :: forall e t. (MsgEncodingI e, Traversable t) => t (ConnOrGroupId, PQSupport, ChatMsgEvent e) -> CM' (t (Either ChatError SndMessage)) +createSndMessages :: forall e t. (MsgEncodingI e, Traversable t) => t (ConnOrGroupId, ChatMsgEvent e) -> CM' (t (Either ChatError SndMessage)) createSndMessages idsEvents = do g <- asks random vr <- chatVersionRange' withStoreBatch $ \db -> fmap (createMsg db g vr) idsEvents where - createMsg :: DB.Connection -> TVar ChaChaDRG -> (PQSupport -> VersionRangeChat) -> (ConnOrGroupId, PQSupport, ChatMsgEvent e) -> IO (Either ChatError SndMessage) - createMsg db g vr (connOrGroupId, pqSup, evnt) = runExceptT $ do + createMsg :: DB.Connection -> TVar ChaChaDRG -> VersionRangeChat -> (ConnOrGroupId, ChatMsgEvent e) -> IO (Either ChatError SndMessage) + createMsg db g vr (connOrGroupId, evnt) = runExceptT $ do withExceptT ChatErrorStore $ createNewSndMessage db g connOrGroupId evnt encodeMessage where encodeMessage sharedMsgId = - encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr pqSup, msgId = Just sharedMsgId, chatMsgEvent = evnt} + encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt} sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> Connection -> NonEmpty (ChatMsgEvent e) -> GroupId -> CM () sendGroupMemberMessages user conn events groupId = do when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) - let idsEvts = L.map (GroupId groupId,PQSupportOff,) events + let idsEvts = L.map (GroupId groupId,) events (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts unless (null errs) $ toView $ CRChatErrors (Just user) errs forM_ (L.nonEmpty msgs) $ \msgs' -> do @@ -6173,12 +6449,12 @@ batchSndMessagesJSON = batchMessages maxEncodedMsgLength . L.toList encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString encodeConnInfo chatMsgEvent = do vr <- chatVersionRange - encodeConnInfoPQ PQSupportOff (maxVersion $ vr PQSupportOff) chatMsgEvent + encodeConnInfoPQ PQSupportOff (maxVersion vr) chatMsgEvent encodeConnInfoPQ :: MsgEncodingI e => PQSupport -> VersionChat -> ChatMsgEvent e -> CM ByteString encodeConnInfoPQ pqSup v chatMsgEvent = do vr <- chatVersionRange - let info = ChatMessage {chatVRange = vr pqSup, msgId = Nothing, chatMsgEvent} + let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent} case encodeChatMessage maxEncodedInfoLength info of ECMEncoded connInfo -> case pqSup of PQSupportOn | v >= pqEncryptionCompressionVersion && B.length connInfo > maxCompressedInfoLength -> do @@ -6265,7 +6541,7 @@ sendGroupMessage user gInfo members chatMsgEvent = do sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, [GroupMember]) sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do - msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId) PQSupportOff + msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId) recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent} (toSend, pending) = foldr addMember ([], []) recipientMembers @@ -6320,7 +6596,7 @@ memberSendAction chatMsgEvent members m@GroupMember {invitedByGroupMemberId} = c sendGroupMemberMessage :: MsgEncodingI e => User -> GroupMember -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> CM () -> CM () sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId introId_ postDeliver = do - msg <- createSndMessage chatMsgEvent (GroupId groupId) PQSupportOff + msg <- createSndMessage chatMsgEvent (GroupId groupId) messageMember msg `catchChatError` (\e -> toView (CRChatError (Just user) e)) where messageMember :: SndMessage -> CM () @@ -6394,17 +6670,17 @@ saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {me _ -> throwError e saveSndChatItem :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> CM (ChatItem c 'MDSnd) -saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False +saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing Nothing False -saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd) -saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemTimed live = do +saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd) +saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemForwarded itemTimed live = do createdAt <- liftIO getCurrentTime ciId <- withStore' $ \db -> do when (ciRequiresAttention content) $ updateChatTs db user cd createdAt - ciId <- createNewSndChatItem db user cd msg content quotedItem itemTimed live createdAt + ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt pure ciId - pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemTimed live createdAt Nothing createdAt + pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live createdAt Nothing createdAt saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv) saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = @@ -6413,18 +6689,18 @@ saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> CIContent 'MDRcv -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDRcv) saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do createdAt <- liftIO getCurrentTime - (ciId, quotedItem) <- withStore' $ \db -> do + (ciId, quotedItem, itemForwarded) <- withStore' $ \db -> do when (ciRequiresAttention content) $ updateChatTs db user cd createdAt - (ciId, quotedItem) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt + r@(ciId, _, _) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - pure (ciId, quotedItem) - pure $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemTimed live brokerTs forwardedByMember createdAt + pure r + pure $ mkChatItem cd ciId content ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live brokerTs forwardedByMember createdAt -mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d -mkChatItem cd ciId content file quotedItem sharedMsgId itemTimed live itemTs forwardedByMember currentTs = +mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d +mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live itemTs forwardedByMember currentTs = let itemText = ciContentToText content itemStatus = ciCreateStatus content - meta = mkCIMeta ciId content itemText itemStatus sharedMsgId Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs + meta = mkCIMeta ciId content itemText itemStatus sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file} deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse @@ -6646,14 +6922,14 @@ createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do cup = getContactUserPreference f cups cup' = getContactUserPreference f cups' -createGroupFeatureChangedItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> CIContent d) -> GroupInfo -> GroupInfo -> CM () +createGroupFeatureChangedItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> GroupInfo -> CM () createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} = forM_ allGroupFeatures $ \(AGF f) -> do let state = groupFeatureState $ getGroupPreference f gps pref' = getGroupPreference f gps' - state'@(_, int') = groupFeatureState pref' + state'@(_, param', role') = groupFeatureState pref' when (state /= state') $ - createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference pref') int') Nothing + createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference pref') param' role') Nothing sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing} @@ -6685,17 +6961,17 @@ createInternalItemsForChats user itemTs_ dirsCIContents = do createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem] createACIs db itemTs createdAt cd = map $ \content -> do ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt - let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs Nothing createdAt + let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing Nothing False itemTs Nothing createdAt pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci -createLocalChatItem :: MsgDirectionI d => User -> ChatDirection 'CTLocal d -> CIContent d -> UTCTime -> CM ChatItemId -createLocalChatItem user cd content createdAt = do +createLocalChatItem :: MsgDirectionI d => User -> ChatDirection 'CTLocal d -> CIContent d -> Maybe CIForwardedFrom -> UTCTime -> CM ChatItemId +createLocalChatItem user cd content itemForwarded createdAt = do gVar <- asks random withStore $ \db -> do liftIO $ updateChatTs db user cd createdAt createWithRandomId gVar $ \sharedMsgId -> let smi_ = Just (SharedMsgId sharedMsgId) - in createNewChatItem_ db user cd Nothing smi_ content (Nothing, Nothing, Nothing, Nothing, Nothing) Nothing False createdAt Nothing createdAt + in createNewChatItem_ db user cd Nothing smi_ content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False createdAt Nothing createdAt withUser' :: (User -> CM ChatResponse) -> CM ChatResponse withUser' action = @@ -6737,11 +7013,11 @@ waitChatStartedAndActivated = do activated <- readTVar chatActivated unless (isJust started && activated) retry -chatVersionRange :: CM (PQSupport -> VersionRangeChat) +chatVersionRange :: CM VersionRangeChat chatVersionRange = lift chatVersionRange' {-# INLINE chatVersionRange #-} -chatVersionRange' :: CM' (PQSupport -> VersionRangeChat) +chatVersionRange' :: CM' VersionRangeChat chatVersionRange' = do ChatConfig {chatVRange} <- asks config pure chatVRange @@ -6784,14 +7060,15 @@ chatCommandP = "/_app activate" $> APIActivateChat True, "/_app suspend " *> (APISuspendChat <$> A.decimal), "/_resubscribe all" $> ResubscribeAllConnections, + -- deprecated, use /set file paths "/_temp_folder " *> (SetTempFolder <$> filePath), + -- /_files_folder deprecated, use /set file paths ("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath), + -- deprecated, use /set file paths "/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath), + "/set file paths " *> (APISetAppFilePaths <$> jsonP), "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP), "/contact_merge " *> (SetContactMergeEnabled <$> onOffP), - "/_pq @" *> (APISetContactPQ <$> A.decimal <* A.space <*> (PQEncryption <$> onOffP)), - "/pq @" *> (SetContactPQ <$> displayName <* A.space <*> (PQEncryption <$> onOffP)), - "/pq " *> (APISetPQEncryption . PQSupport <$> onOffP), "/_db export " *> (APIExportArchive <$> jsonP), "/db export" $> ExportArchive, "/_db import " *> (APIImportArchive <$> jsonP), @@ -6822,6 +7099,7 @@ chatCommandP = "/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode), "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal), "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP), + "/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, "/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))), @@ -6844,6 +7122,8 @@ chatCommandP = "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP), + "/_set theme user " *> (APISetUserUIThemes <$> A.decimal <*> optional (A.space *> jsonP)), + "/_set theme " *> (APISetChatUIThemes <$> chatRefP <*> optional (A.space *> jsonP)), "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString), "/_ntf get" $> APIGetNtfToken, "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP), @@ -6873,6 +7153,7 @@ chatCommandP = "/ttl " *> (SetChatItemTTL <$> ciTTL), "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), "/ttl" $> GetChatItemTTL, + "/_network info " *> (APISetNetworkInfo <$> jsonP), "/_network " *> (APISetNetworkConfig <$> jsonP), ("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP), ("/network" <|> "/net") $> APIGetNetworkConfig, @@ -6963,6 +7244,10 @@ chatCommandP = "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), ("/connect" <|> "/c") *> (AddContact <$> incognitoP), + ForwardMessage <$> chatNameP <* " <- @" <*> displayName <* A.space <*> msgTextP, + ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayName <* A.space <* A.char '@' <*> (Just <$> displayName) <* A.space <*> msgTextP, + ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayName <*> pure Nothing <* A.space <*> msgTextP, + ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, SendMessage <$> chatNameP <* A.space <*> msgTextP, "/* " *> (SendMessage (ChatName CTLocal "") <$> msgTextP), "@#" *> (SendMemberContactMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> msgTextP), @@ -7011,20 +7296,22 @@ chatCommandP = "/show profile image" $> ShowProfileImage, ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNames), ("/profile" <|> "/p") $> ShowProfile, - "/set voice #" *> (SetGroupFeature (AGF SGFVoice) <$> displayName <*> (A.space *> strP)), + "/set voice #" *> (SetGroupFeatureRole (AGFR SGFVoice) <$> displayName <*> _strP <*> optional memberRole), "/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)), "/set voice " *> (SetUserFeature (ACF SCFVoice) <$> strP), - "/set files #" *> (SetGroupFeature (AGF SGFFiles) <$> displayName <*> (A.space *> strP)), - "/set history #" *> (SetGroupFeature (AGF SGFHistory) <$> displayName <*> (A.space *> strP)), + "/set files #" *> (SetGroupFeatureRole (AGFR SGFFiles) <$> displayName <*> _strP <*> optional memberRole), + "/set history #" *> (SetGroupFeature (AGFNR SGFHistory) <$> displayName <*> (A.space *> strP)), + "/set reactions #" *> (SetGroupFeature (AGFNR SGFReactions) <$> displayName <*> (A.space *> strP)), "/set calls @" *> (SetContactFeature (ACF SCFCalls) <$> displayName <*> optional (A.space *> strP)), "/set calls " *> (SetUserFeature (ACF SCFCalls) <$> strP), - "/set delete #" *> (SetGroupFeature (AGF SGFFullDelete) <$> displayName <*> (A.space *> strP)), + "/set delete #" *> (SetGroupFeature (AGFNR SGFFullDelete) <$> displayName <*> (A.space *> strP)), "/set delete @" *> (SetContactFeature (ACF SCFFullDelete) <$> displayName <*> optional (A.space *> strP)), "/set delete " *> (SetUserFeature (ACF SCFFullDelete) <$> strP), - "/set direct #" *> (SetGroupFeature (AGF SGFDirectMessages) <$> displayName <*> (A.space *> strP)), + "/set direct #" *> (SetGroupFeatureRole (AGFR SGFDirectMessages) <$> displayName <*> _strP <*> optional memberRole), "/set disappear #" *> (SetGroupTimedMessages <$> displayName <*> (A.space *> timedTTLOnOffP)), "/set disappear @" *> (SetContactTimedMessages <$> displayName <*> optional (A.space *> timedMessagesEnabledP)), "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), + "/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayName <*> _strP <*> optional memberRole), ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, "/set device name " *> (SetLocalDeviceName <$> textP), "/list remote hosts" $> ListRemoteHosts, @@ -7047,6 +7334,7 @@ chatCommandP = ("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/version" <|> "/v") $> ShowVersion, "/debug locks" $> DebugLocks, + "/debug event " *> (DebugEvent <$> jsonP), "/get stats" $> GetAgentStats, "/reset stats" $> ResetAgentStats, "/get subs" $> GetAgentSubs, @@ -7112,7 +7400,7 @@ chatCommandP = let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) - { directMessages = Just DirectMessagesGroupPreference {enable = FEOn}, + { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, history = Just HistoryGroupPreference {enable = FEOn} } pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences} diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs index 572ce0c67b..6996cc1d87 100644 --- a/src/Simplex/Chat/AppSettings.hs +++ b/src/Simplex/Chat/AppSettings.hs @@ -11,6 +11,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ import Data.Maybe (fromMaybe) import Data.Text (Text) +import Simplex.Chat.Types.UITheme import Simplex.Messaging.Client (NetworkConfig, defaultNetworkConfig) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Util (catchAll_) @@ -43,7 +44,11 @@ data AppSettings = AppSettings confirmDBUpgrades :: Maybe Bool, androidCallOnLockScreen :: Maybe LockScreenCalls, iosCallKitEnabled :: Maybe Bool, - iosCallKitCallsInRecents :: Maybe Bool + iosCallKitCallsInRecents :: Maybe Bool, + uiProfileImageCornerRadius :: Maybe Double, + uiColorScheme :: Maybe UIColorScheme, + uiDarkColorScheme :: Maybe DarkColorScheme, + uiThemes :: Maybe UIThemes } deriving (Show) @@ -69,7 +74,11 @@ defaultAppSettings = confirmDBUpgrades = Just False, androidCallOnLockScreen = Just LSCShow, iosCallKitEnabled = Just True, - iosCallKitCallsInRecents = Just False + iosCallKitCallsInRecents = Just False, + uiProfileImageCornerRadius = Just 22.5, + uiColorScheme = Just UCSSystem, + uiDarkColorScheme = Just DCSSimplex, + uiThemes = Nothing } defaultParseAppSettings :: AppSettings @@ -94,13 +103,17 @@ defaultParseAppSettings = confirmDBUpgrades = Nothing, androidCallOnLockScreen = Nothing, iosCallKitEnabled = Nothing, - iosCallKitCallsInRecents = Nothing + iosCallKitCallsInRecents = Nothing, + uiProfileImageCornerRadius = Nothing, + uiColorScheme = Nothing, + uiDarkColorScheme = Nothing, + uiThemes = Nothing } combineAppSettings :: AppSettings -> AppSettings -> AppSettings combineAppSettings platformDefaults storedSettings = AppSettings - { appPlatform = p appPlatform, + { appPlatform = p appPlatform, networkConfig = p networkConfig, privacyEncryptLocalFiles = p privacyEncryptLocalFiles, privacyAcceptImages = p privacyAcceptImages, @@ -119,7 +132,11 @@ combineAppSettings platformDefaults storedSettings = confirmDBUpgrades = p confirmDBUpgrades, iosCallKitEnabled = p iosCallKitEnabled, iosCallKitCallsInRecents = p iosCallKitCallsInRecents, - androidCallOnLockScreen = p androidCallOnLockScreen + androidCallOnLockScreen = p androidCallOnLockScreen, + uiProfileImageCornerRadius = p uiProfileImageCornerRadius, + uiColorScheme = p uiColorScheme, + uiDarkColorScheme = p uiDarkColorScheme, + uiThemes = p uiThemes } where p :: (AppSettings -> Maybe a) -> Maybe a @@ -157,6 +174,10 @@ instance FromJSON AppSettings where iosCallKitEnabled <- p "iosCallKitEnabled" iosCallKitCallsInRecents <- p "iosCallKitCallsInRecents" androidCallOnLockScreen <- p "androidCallOnLockScreen" + uiProfileImageCornerRadius <- p "uiProfileImageCornerRadius" + uiColorScheme <- p "uiColorScheme" + uiDarkColorScheme <- p "uiDarkColorScheme" + uiThemes <- p "uiThemes" pure AppSettings { appPlatform, @@ -178,7 +199,11 @@ instance FromJSON AppSettings where confirmDBUpgrades, iosCallKitEnabled, iosCallKitCallsInRecents, - androidCallOnLockScreen + androidCallOnLockScreen, + uiProfileImageCornerRadius, + uiColorScheme, + uiDarkColorScheme, + uiThemes } where p key = v .:? key <|> pure Nothing diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index d51f60f5f1..8550c03438 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -44,14 +45,22 @@ archiveChatDbFile = "simplex_v1_chat.db" archiveFilesFolder :: String archiveFilesFolder = "simplex_v1_files" +archiveAssetsFolder :: String +archiveAssetsFolder = "simplex_v1_assets" + +wallpapersFolder :: String +wallpapersFolder = "wallpapers" + exportArchive :: ArchiveConfig -> CM' () exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = withTempDir cfg "simplex-chat." $ \dir -> do - StorageFiles {chatStore, agentStore, filesPath} <- storageFiles + StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles copyFile (dbFilePath chatStore) $ dir archiveChatDbFile copyFile (dbFilePath agentStore) $ dir archiveAgentDbFile forM_ filesPath $ \fp -> copyDirectoryFiles fp $ dir archiveFilesFolder + forM_ assetsPath $ \fp -> + copyDirectoryFiles (fp wallpapersFolder) $ dir archiveAssetsFolder wallpapersFolder let method = if disableCompression == Just True then Z.Store else Z.Deflate Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir @@ -59,24 +68,24 @@ importArchive :: ArchiveConfig -> CM' [ArchiveError] importArchive cfg@ArchiveConfig {archivePath} = withTempDir cfg "simplex-chat." $ \dir -> do Z.withArchive archivePath $ Z.unpackInto dir - fs@StorageFiles {chatStore, agentStore, filesPath} <- storageFiles + fs@StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles liftIO $ closeSQLiteStore `withStores` fs backup `withDBs` fs copyFile (dir archiveChatDbFile) $ dbFilePath chatStore copyFile (dir archiveAgentDbFile) $ dbFilePath agentStore - copyFiles dir filesPath - `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e] + errs <- copyFiles (dir archiveFilesFolder) filesPath + errs' <- copyFiles (dir archiveAssetsFolder wallpapersFolder) (( wallpapersFolder) <$> assetsPath) + pure $ errs <> errs' where backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak" - copyFiles dir filesPath = do - let filesDir = dir archiveFilesFolder - case filesPath of - Just fp -> - ifM - (doesDirectoryExist filesDir) - (copyDirectoryFiles filesDir fp) - (pure []) - _ -> pure [] + copyFiles fromDir = \case + Just fp -> + ifM + (doesDirectoryExist fromDir) + (copyDirectoryFiles fromDir fp) + (pure []) + `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e] + _ -> pure [] withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a) withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of @@ -85,7 +94,7 @@ withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError] copyDirectoryFiles fromDir toDir = do - createDirectoryIfMissing False toDir + createDirectoryIfMissing True toDir fs <- listDirectory fromDir foldM copyFileCatchError [] fs where @@ -103,6 +112,7 @@ deleteStorage = do liftIO $ closeSQLiteStore `withStores` fs remove `withDBs` fs mapM_ removeDir $ filesPath fs + mapM_ removeDir $ assetsPath fs mapM_ removeDir =<< chatReadVar tempDirectory where remove f = whenM (doesFileExist f) $ removeFile f @@ -111,15 +121,17 @@ deleteStorage = do data StorageFiles = StorageFiles { chatStore :: SQLiteStore, agentStore :: SQLiteStore, - filesPath :: Maybe FilePath + filesPath :: Maybe FilePath, + assetsPath :: Maybe FilePath } storageFiles :: CM' StorageFiles storageFiles = do - ChatController {chatStore, filesFolder, smpAgent} <- ask + ChatController {chatStore, filesFolder, assetsDirectory, smpAgent} <- ask let agentStore = agentClientStore smpAgent filesPath <- readTVarIO filesFolder - pure StorageFiles {chatStore, agentStore, filesPath} + assetsPath <- readTVarIO assetsDirectory + pure StorageFiles {chatStore, agentStore, filesPath, assetsPath} sqlCipherExport :: DBEncryptionConfig -> CM () sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key', keepKey} = @@ -177,9 +189,9 @@ testSQL k = T.unlines $ keySQL k <> [ "PRAGMA foreign_keys = ON;", - "PRAGMA secure_delete = ON;", - "SELECT count(*) FROM sqlite_master;" - ] + "PRAGMA secure_delete = ON;", + "SELECT count(*) FROM sqlite_master;" + ] keySQL :: BA.ScrubbedBytes -> [Text] keySQL k = ["PRAGMA key = " <> keyString k <> ";" | not (BA.null k)] diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0291793843..26ace9c772 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -59,13 +59,15 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types -import Simplex.Chat.Store (AutoAccept, StoreError (..), UserContactLink, UserMsgReceiptSettings) +import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) -import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure) +import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol @@ -75,7 +77,7 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF -import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport (..)) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) @@ -125,7 +127,7 @@ coreVersionInfo simplexmqCommit = data ChatConfig = ChatConfig { agentConfig :: AgentConfig, - chatVRange :: PQSupport -> VersionRangeChat, + chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, defaultServers :: DefaultAgentServers, tbqSize :: Natural, @@ -165,7 +167,7 @@ defaultChatHooks = ChatHooks { preCmdHook = \_ -> pure . Right, eventHook = \_ -> pure - } + } data DefaultAgentServers = DefaultAgentServers { smp :: NonEmpty SMPServerWithAuth, @@ -208,6 +210,7 @@ data ChatController = ChatController connNetworkStatuses :: TMap AgentConnId NetworkStatus, subscriptionMode :: TVar SubscriptionMode, chatLock :: Lock, + entityLocks :: TMap ChatLockEntity Lock, sndFiles :: TVar (Map Int64 Handle), rcvFiles :: TVar (Map Int64 Handle), currentCalls :: TMap ContactId Call, @@ -227,9 +230,9 @@ data ChatController = ChatController showLiveItems :: TVar Bool, encryptLocalFiles :: TVar Bool, tempDirectory :: TVar (Maybe FilePath), + assetsDirectory :: TVar (Maybe FilePath), logFilePath :: Maybe FilePath, - contactMergeEnabled :: TVar Bool, - pqExperimentalEnabled :: TVar PQSupport -- TODO v5.7 remove + contactMergeEnabled :: TVar Bool } data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSRemote | HSSettings | HSDatabase @@ -264,11 +267,9 @@ data ChatCommand | SetTempFolder FilePath | SetFilesFolder FilePath | SetRemoteHostsFolder FilePath + | APISetAppFilePaths AppFilePathsConfig | APISetEncryptLocalFiles Bool | SetContactMergeEnabled Bool - | APISetPQEncryption PQSupport - | APISetContactPQ ContactId PQEncryption - | SetContactPQ ContactName PQEncryption | APIExportArchive ArchiveConfig | ExportArchive | APIImportArchive ArchiveConfig @@ -290,6 +291,7 @@ data ChatCommand | APIDeleteChatItem ChatRef ChatItemId CIDeleteMode | APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} + | APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId} | APIUserRead UserId | UserRead | APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId)) @@ -312,6 +314,8 @@ data ChatCommand | APISetContactPrefs ContactId Preferences | APISetContactAlias ContactId LocalAlias | APISetConnectionAlias Int64 LocalAlias + | APISetUserUIThemes UserId (Maybe UIThemeEntityOverrides) + | APISetChatUIThemes ChatRef (Maybe UIThemeEntityOverrides) | APIParseMarkdown Text | APIGetNtfToken | APIRegisterToken DeviceToken NotificationsMode @@ -344,6 +348,7 @@ data ChatCommand | GetChatItemTTL | APISetNetworkConfig NetworkConfig | APIGetNetworkConfig + | APISetNetworkInfo UserNetworkInfo | ReconnectAllServers | APISetChatSettings ChatRef ChatSettings | APISetMemberSettings GroupId GroupMemberId GroupMemberSettings @@ -406,6 +411,9 @@ data ChatCommand | AddressAutoAccept (Maybe AutoAccept) | AcceptContact IncognitoEnabled ContactName | RejectContact ContactName + | ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text} + | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} + | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} | SendMessage ChatName Text | SendMemberContactMessage GroupName ContactName Text | SendLiveMessage ChatName Text @@ -460,7 +468,8 @@ data ChatCommand | ShowProfileImage | SetUserFeature AChatFeature FeatureAllowed -- UserId (not used in UI) | SetContactFeature AChatFeature ContactName (Maybe FeatureAllowed) - | SetGroupFeature AGroupFeature GroupName GroupFeatureEnabled + | SetGroupFeature AGroupFeatureNoRole GroupName GroupFeatureEnabled + | SetGroupFeatureRole AGroupFeatureRole GroupName GroupFeatureEnabled (Maybe GroupMemberRole) | SetUserTimedMessages Bool -- UserId (not used in UI) | SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled) | SetGroupTimedMessages GroupName (Maybe Int) @@ -485,15 +494,16 @@ data ChatCommand | QuitChat | ShowVersion | DebugLocks + | DebugEvent ChatResponse | GetAgentStats | ResetAgentStats | GetAgentSubs | GetAgentSubsDetails | GetAgentWorkers | GetAgentWorkersDetails - -- The parser will return this command for strings that start from "//". - -- This command should be processed in preCmdHook - | CustomChatCommand ByteString + | -- The parser will return this command for strings that start from "//". + -- This command should be processed in preCmdHook + CustomChatCommand ByteString deriving (Show) allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal @@ -727,11 +737,10 @@ data ChatResponse | CRRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo} | CRRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason} - | CRContactPQAllowed {user :: User, contact :: Contact, pqEncryption :: PQEncryption} | CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} | CRSQLResult {rows :: [Text]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} - | CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks} + | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} | CRAgentStats {agentStats :: [[String]]} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} | CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary} @@ -924,6 +933,14 @@ instance StrEncoding DBEncryptionKey where instance FromJSON DBEncryptionKey where parseJSON = strParseJSON "DBEncryptionKey" +data AppFilePathsConfig = AppFilePathsConfig + { appFilesFolder :: FilePath, + appTempFolder :: FilePath, + appAssetsFolder :: FilePath, + appRemoteHostsFolder :: Maybe FilePath + } + deriving (Show) + data ContactSubStatus = ContactSubStatus { contact :: Contact, contactError :: Maybe ChatError @@ -1111,6 +1128,8 @@ data ChatErrorType | CEFallbackToSMPProhibited {fileId :: FileTransferId} | CEInlineFileProhibited {fileId :: FileTransferId} | CEInvalidQuote + | CEInvalidForward + | CEForwardNoFile | CEInvalidChatItemUpdate | CEInvalidChatItemDelete | CEHasCurrentCall @@ -1353,7 +1372,7 @@ handleDBErrors = [ E.Handler $ \(e :: SQLError) -> let se = SQL.sqlError e busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked - in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e, + in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e, E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e ] @@ -1393,6 +1412,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) +$(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) + $(JQ.deriveJSON defaultJSON ''ContactSubStatus) $(JQ.deriveJSON defaultJSON ''MemberSubStatus) diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 2eabb48166..d3b9ea52f1 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -144,6 +144,15 @@ markdownToList (m1 :|: m2) = markdownToList m1 <> markdownToList m2 parseMarkdown :: Text -> Markdown parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s +containsFormat :: (Format -> Bool) -> Markdown -> Bool +containsFormat p (Markdown f _) = maybe False p f +containsFormat p (m1 :|: m2) = containsFormat p m1 || containsFormat p m2 + +isSimplexLink :: Format -> Bool +isSimplexLink = \case + SimplexLink {} -> True; + _ -> False + markdownP :: Parser Markdown markdownP = mconcat <$> A.many' fragmentP where diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index b29543cf74..a6d5761b5f 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -339,10 +339,12 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta itemText :: Text, itemStatus :: CIStatus d, itemSharedMsgId :: Maybe SharedMsgId, + itemForwarded :: Maybe CIForwardedFrom, itemDeleted :: Maybe (CIDeleted c), itemEdited :: Bool, itemTimed :: Maybe CITimed, itemLive :: Maybe Bool, + deletable :: Bool, editable :: Bool, forwardedByMember :: Maybe GroupMemberId, createdAt :: UTCTime, @@ -350,15 +352,16 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta } deriving (Show) -mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d -mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = - let editable = case itemContent of +mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d +mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = + let deletable = case itemContent of CISndMsgContent _ -> case chatTypeI @c of SCTLocal -> isNothing itemDeleted _ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted _ -> False - in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, itemTimed, itemLive, editable, forwardedByMember, createdAt, updatedAt} + editable = deletable && isNothing itemForwarded + in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt} dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd dummyMeta itemId ts itemText = @@ -368,10 +371,12 @@ dummyMeta itemId ts itemText = itemText, itemStatus = CISSndNew, itemSharedMsgId = Nothing, + itemForwarded = Nothing, itemDeleted = Nothing, itemEdited = False, itemTimed = Nothing, itemLive = Nothing, + deletable = False, editable = False, forwardedByMember = Nothing, createdAt = ts, @@ -548,6 +553,21 @@ ciFileEnded = \case CIFSRcvError -> True CIFSInvalid {} -> True +ciFileLoaded :: CIFileStatus d -> Bool +ciFileLoaded = \case + CIFSSndStored -> True + CIFSSndTransfer {} -> True + CIFSSndComplete -> True + CIFSSndCancelled -> True + CIFSSndError -> True + CIFSRcvInvitation -> False + CIFSRcvAccepted -> False + CIFSRcvTransfer {} -> False + CIFSRcvCancelled -> False + CIFSRcvComplete -> True + CIFSRcvError -> False + CIFSInvalid {} -> False + data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d) deriving instance Show ACIFileStatus @@ -981,11 +1001,43 @@ itemDeletedTs = \case CIBlockedByAdmin ts -> ts CIModerated ts _ -> ts +data CIForwardedFrom + = CIFFUnknown + | CIFFContact {chatName :: Text, msgDir :: MsgDirection, contactId :: Maybe ContactId, chatItemId :: Maybe ChatItemId} + | CIFFGroup {chatName :: Text, msgDir :: MsgDirection, groupId :: Maybe GroupId, chatItemId :: Maybe ChatItemId} + deriving (Show) + +cmForwardedFrom :: AChatMsgEvent -> Maybe CIForwardedFrom +cmForwardedFrom = \case + ACME _ (XMsgNew (MCForward _)) -> Just CIFFUnknown + _ -> Nothing + +data CIForwardedFromTag + = CIFFUnknown_ + | CIFFContact_ + | CIFFGroup_ + +instance FromField CIForwardedFromTag where fromField = fromTextField_ textDecode + +instance ToField CIForwardedFromTag where toField = toField . textEncode + +instance TextEncoding CIForwardedFromTag where + textDecode = \case + "unknown" -> Just CIFFUnknown_ + "contact" -> Just CIFFContact_ + "group" -> Just CIFFGroup_ + _ -> Nothing + textEncode = \case + CIFFUnknown_ -> "unknown" + CIFFContact_ -> "contact" + CIFFGroup_ -> "group" + data ChatItemInfo = ChatItemInfo { itemVersions :: [ChatItemVersion], - memberDeliveryStatuses :: Maybe [MemberDeliveryStatus] + memberDeliveryStatuses :: Maybe [MemberDeliveryStatus], + forwardedFromChatItem :: Maybe AChatItem } - deriving (Eq, Show) + deriving (Show) data ChatItemVersion = ChatItemVersion { chatItemVersionId :: Int64, @@ -1043,6 +1095,8 @@ instance ChatTypeI c => ToJSON (CIDeleted c) where toJSON = J.toJSON . jsonCIDeleted toEncoding = J.toEncoding . jsonCIDeleted +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CIFF") ''CIForwardedFrom) + $(JQ.deriveJSON defaultJSON ''CITimed) $(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress) @@ -1066,8 +1120,6 @@ $(JQ.deriveJSON defaultJSON ''MemberDeliveryStatus) $(JQ.deriveJSON defaultJSON ''ChatItemVersion) -$(JQ.deriveJSON defaultJSON ''ChatItemInfo) - instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIMeta c d) where parseJSON = $(JQ.mkParseJSON defaultJSON ''CIMeta) @@ -1157,6 +1209,8 @@ instance ChatTypeI c => ToJSON (CChatItem c) where toJSON (CChatItem _ ci) = J.toJSON ci toEncoding (CChatItem _ ci) = J.toEncoding ci +$(JQ.deriveJSON defaultJSON ''ChatItemInfo) + $(JQ.deriveJSON defaultJSON ''ChatStats) instance ChatTypeI c => ToJSON (Chat c) where diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 0e95570b85..3d71047d57 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -28,6 +28,7 @@ import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), RatchetSyncState (..), SwitchPhase (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOn, pattern PQEncOff) @@ -42,6 +43,8 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "MD") ''MsgDirection) instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgDirection . msgDirectionIntP +instance FromField MsgDirection where fromField = fromIntField_ msgDirectionIntP + instance ToField MsgDirection where toField = toField . msgDirectionInt data SMsgDirection (d :: MsgDirection) where @@ -134,8 +137,8 @@ data CIContent (d :: MsgDirection) where CISndChatFeature :: ChatFeature -> PrefEnabled -> Maybe Int -> CIContent 'MDSnd CIRcvChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDRcv CISndChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDSnd - CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDRcv - CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDSnd + CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent 'MDRcv + CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent 'MDSnd CIRcvChatFeatureRejected :: ChatFeature -> CIContent 'MDRcv CIRcvGroupFeatureRejected :: GroupFeature -> CIContent 'MDRcv CISndModerated :: CIContent 'MDSnd @@ -255,8 +258,8 @@ ciContentToText = \case CISndChatFeature feature enabled param -> featureStateText feature enabled param CIRcvChatPreference feature allowed param -> prefStateText feature allowed param CISndChatPreference feature allowed param -> "you " <> prefStateText feature allowed param - CIRcvGroupFeature feature pref param -> groupPrefStateText feature pref param - CISndGroupFeature feature pref param -> groupPrefStateText feature pref param + CIRcvGroupFeature feature pref param role -> groupPrefStateText feature pref param role + CISndGroupFeature feature pref param role -> groupPrefStateText feature pref param role CIRcvChatFeatureRejected feature -> chatFeatureNameText feature <> ": received, prohibited" CIRcvGroupFeatureRejected feature -> groupFeatureNameText feature <> ": received, prohibited" CISndModerated -> ciModeratedText @@ -413,8 +416,8 @@ data JSONCIContent | JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} | JCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} | JCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} + | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} | JCIRcvChatFeatureRejected {feature :: ChatFeature} | JCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} | JCISndModerated @@ -447,8 +450,8 @@ jsonCIContent = \case CISndChatFeature feature enabled param -> JCISndChatFeature {feature, enabled, param} CIRcvChatPreference feature allowed param -> JCIRcvChatPreference {feature, allowed, param} CISndChatPreference feature allowed param -> JCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> JCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> JCISndGroupFeature {groupFeature, preference, param} + CIRcvGroupFeature groupFeature preference param memberRole_ -> JCIRcvGroupFeature {groupFeature, preference, param, memberRole_} + CISndGroupFeature groupFeature preference param memberRole_ -> JCISndGroupFeature {groupFeature, preference, param, memberRole_} CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> JCIRcvGroupFeatureRejected {groupFeature} CISndModerated -> JCISndModerated @@ -481,8 +484,8 @@ aciContentJSON = \case JCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param JCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param JCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - JCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - JCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + JCIRcvGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param memberRole_ + JCISndGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param memberRole_ JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature JCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature JCISndModerated -> ACIContent SMDSnd CISndModerated @@ -516,8 +519,8 @@ data DBJSONCIContent | DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} | DBJCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} | DBJCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} + | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} | DBJCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} | DBJCISndModerated @@ -550,8 +553,8 @@ dbJsonCIContent = \case CISndChatFeature feature enabled param -> DBJCISndChatFeature {feature, enabled, param} CIRcvChatPreference feature allowed param -> DBJCIRcvChatPreference {feature, allowed, param} CISndChatPreference feature allowed param -> DBJCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> DBJCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> DBJCISndGroupFeature {groupFeature, preference, param} + CIRcvGroupFeature groupFeature preference param memberRole_ -> DBJCIRcvGroupFeature {groupFeature, preference, param, memberRole_} + CISndGroupFeature groupFeature preference param memberRole_ -> DBJCISndGroupFeature {groupFeature, preference, param, memberRole_} CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> DBJCIRcvGroupFeatureRejected {groupFeature} CISndModerated -> DBJCISndModerated @@ -584,8 +587,8 @@ aciContentDBJSON = \case DBJCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param DBJCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param DBJCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - DBJCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - DBJCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + DBJCIRcvGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param memberRole_ + DBJCISndGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param memberRole_ DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature DBJCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature DBJCISndModerated -> ACIContent SMDSnd CISndModerated @@ -620,6 +623,14 @@ ciCallInfoText status duration = case status of CISCallEnded -> "ended " <> durationText duration CISCallError -> "error" +callComplete :: CICallStatus -> Bool +callComplete = \case + CISCallMissed -> True + CISCallRejected -> True + CISCallEnded -> True + CISCallError -> True + _ -> False + $(JQ.deriveJSON defaultJSON ''E2EInfo) $(JQ.deriveJSON (enumJSON $ dropPrefix "MDE") ''MsgDecryptError) diff --git a/src/Simplex/Chat/Messages/CIContent/Events.hs b/src/Simplex/Chat/Messages/CIContent/Events.hs index 7ce5f73cde..74f7d94399 100644 --- a/src/Simplex/Chat/Messages/CIContent/Events.hs +++ b/src/Simplex/Chat/Messages/CIContent/Events.hs @@ -7,6 +7,7 @@ module Simplex.Chat.Messages.CIContent.Events where import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson.TH as J import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (RatchetSyncState (..), SwitchPhase (..)) import Simplex.Messaging.Parsers (dropPrefix, singleFieldJSON, sumTypeJSON) import Simplex.Messaging.Crypto.Ratchet (PQEncryption) diff --git a/src/Simplex/Chat/Migrations/M20240402_item_forwarded.hs b/src/Simplex/Chat/Migrations/M20240402_item_forwarded.hs new file mode 100644 index 0000000000..850c8be2d9 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240402_item_forwarded.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240402_item_forwarded where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240402_item_forwarded :: Query +m20240402_item_forwarded = + [sql| +ALTER TABLE chat_items ADD COLUMN fwd_from_tag TEXT; +ALTER TABLE chat_items ADD COLUMN fwd_from_chat_name TEXT; +ALTER TABLE chat_items ADD COLUMN fwd_from_msg_dir INTEGER; +ALTER TABLE chat_items ADD COLUMN fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL; +ALTER TABLE chat_items ADD COLUMN fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL; +ALTER TABLE chat_items ADD COLUMN fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL; + +CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items(fwd_from_contact_id); +CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id); +CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(fwd_from_chat_item_id); +|] + +down_m20240402_item_forwarded :: Query +down_m20240402_item_forwarded = + [sql| +DROP INDEX idx_chat_items_fwd_from_contact_id; +DROP INDEX idx_chat_items_fwd_from_group_id; +DROP INDEX idx_chat_items_fwd_from_chat_item_id; + +ALTER TABLE chat_items DROP COLUMN fwd_from_tag; +ALTER TABLE chat_items DROP COLUMN fwd_from_chat_name; +ALTER TABLE chat_items DROP COLUMN fwd_from_msg_dir; +ALTER TABLE chat_items DROP COLUMN fwd_from_contact_id; +ALTER TABLE chat_items DROP COLUMN fwd_from_group_id; +ALTER TABLE chat_items DROP COLUMN fwd_from_chat_item_id; +|] diff --git a/src/Simplex/Chat/Migrations/M20240430_ui_theme.hs b/src/Simplex/Chat/Migrations/M20240430_ui_theme.hs new file mode 100644 index 0000000000..1f4b9805cf --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240430_ui_theme.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240430_ui_theme where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240430_ui_theme :: Query +m20240430_ui_theme = + [sql| +ALTER TABLE users ADD COLUMN ui_themes TEXT; +ALTER TABLE contacts ADD COLUMN ui_themes TEXT; +ALTER TABLE groups ADD COLUMN ui_themes TEXT; +|] + +down_m20240430_ui_theme :: Query +down_m20240430_ui_theme = + [sql| +ALTER TABLE users DROP COLUMN ui_themes; +ALTER TABLE contacts DROP COLUMN ui_themes; +ALTER TABLE groups DROP COLUMN ui_themes; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 11cbd8ae89..f2f94d019c 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -34,7 +34,8 @@ CREATE TABLE users( show_ntfs INTEGER NOT NULL DEFAULT 1, send_rcpts_contacts INTEGER NOT NULL DEFAULT 0, send_rcpts_small_groups INTEGER NOT NULL DEFAULT 0, - user_member_profile_updated_at TEXT, -- 1 for active user + user_member_profile_updated_at TEXT, + ui_themes TEXT, -- 1 for active user FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE RESTRICT @@ -74,6 +75,7 @@ CREATE TABLE contacts( contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0, contact_status TEXT NOT NULL DEFAULT 'active', custom_data BLOB, + ui_themes TEXT, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -122,7 +124,8 @@ CREATE TABLE groups( send_rcpts INTEGER, via_group_link_uri_hash BLOB, user_member_profile_sent_at TEXT, - custom_data BLOB, -- received + custom_data BLOB, + ui_themes TEXT, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -382,7 +385,13 @@ CREATE TABLE chat_items( item_deleted_ts TEXT, forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, item_content_tag TEXT, - note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE + note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE, + fwd_from_tag TEXT, + fwd_from_chat_name TEXT, + fwd_from_msg_dir INTEGER, + fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL, + fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL, + fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL ); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, @@ -860,3 +869,10 @@ CREATE INDEX idx_chat_items_notes_item_status on chat_items( item_status ); CREATE INDEX idx_files_redirect_file_id on files(redirect_file_id); +CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items( + fwd_from_contact_id +); +CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id); +CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items( + fwd_from_chat_item_id +); diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index e2810dafa9..8c5a9e1905 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -45,10 +45,10 @@ import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Call import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) -import Simplex.Messaging.Compression (compress1, decompressBatch) -import Simplex.Messaging.Crypto.Ratchet (PQSupport (..), pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Compression (Compressed, compress1, decompress1) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) @@ -69,14 +69,11 @@ import Simplex.Messaging.Version hiding (version) -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 7 +currentChatVersion = VersionChat 8 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) --- TODO remove parameterization in 5.7 -supportedChatVRange :: PQSupport -> VersionRangeChat -supportedChatVRange pq = mkVersionRange initialChatVersion $ case pq of - PQSupportOn -> pqEncryptionCompressionVersion - PQSupportOff -> currentChatVersion +supportedChatVRange :: VersionRangeChat +supportedChatVRange = mkVersionRange initialChatVersion currentChatVersion {-# INLINE supportedChatVRange #-} -- version range that supports skipping establishing direct connections in a group and establishing direct connection via x.grp.direct.inv @@ -573,8 +570,7 @@ parseChatMessages s = case B.head s of decodeCompressed :: ByteString -> [Either String AChatMessage] decodeCompressed s' = case smpDecode s' of Left e -> [Left e] - -- TODO v5.7 don't reserve multiple large buffers when decoding batches - Right compressed -> concatMap (either (pure . Left) parseChatMessages) . L.toList $ decompressBatch maxEncodedMsgLength compressed + Right (compressed :: L.NonEmpty Compressed) -> concatMap (either (pure . Left) parseChatMessages . decompress1) compressed compressedBatchMsgBody_ :: MsgBody -> ByteString compressedBatchMsgBody_ = markCompressedBatch . smpEncode . (L.:| []) . compress1 @@ -587,6 +583,7 @@ parseMsgContainer :: J.Object -> JT.Parser MsgContainer parseMsgContainer v = MCQuote <$> v .: "quote" <*> mc <|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc) + <|> (MCForward <$> ((v .: "forward" :: JT.Parser J.Object) *> mc)) <|> MCSimple <$> mc where mc = ExtMsgContent <$> v .: "content" <*> v .:? "file" <*> v .:? "ttl" <*> v .:? "live" diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 819c1cd670..e8d13402ef 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -50,7 +50,7 @@ import Simplex.Chat.Store.Files import Simplex.Chat.Store.Remote import Simplex.Chat.Store.Shared import Simplex.Chat.Types -import Simplex.Chat.Util (liftIOEither, encryptFile) +import Simplex.Chat.Util (encryptFile, liftIOEither) import Simplex.FileTransfer.Description (FileDigest (..)) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP)) @@ -72,11 +72,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [5, 6, 0, 0] +minRemoteCtrlVersion = AppVersion [5, 7, 0, 3] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [5, 6, 0, 0] +minRemoteHostVersion = AppVersion [5, 7, 0, 3] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Remote/RevHTTP.hs b/src/Simplex/Chat/Remote/RevHTTP.hs index 44f3e50b0d..4df5bcac2a 100644 --- a/src/Simplex/Chat/Remote/RevHTTP.hs +++ b/src/Simplex/Chat/Remote/RevHTTP.hs @@ -21,9 +21,9 @@ attachRevHTTP2Client disconnected = attachHTTP2Client config ANY_ADDR_V4 "0" dis attachHTTP2Server :: TLS -> (HTTP2Request -> IO ()) -> IO () attachHTTP2Server tls processRequest = - runHTTP2ServerWith defaultHTTP2BufferSize ($ tls) $ \sessionId r sendResponse -> do + runHTTP2ServerWith defaultHTTP2BufferSize ($ tls) $ \sessionId sessionALPN r sendResponse -> do reqBody <- getHTTP2Body r doNotPrefetchHead - processRequest HTTP2Request {sessionId, request = r, reqBody, sendResponse} + processRequest HTTP2Request {sessionId, sessionALPN, request = r, reqBody, sendResponse} -- | Suppress storing initial chunk in bodyHead, forcing clients and servers to stream chunks doNotPrefetchHead :: Int diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 91021713b1..4b0591fb3a 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -1,6 +1,7 @@ module Simplex.Chat.Store ( SQLiteStore, StoreError (..), + ChatLockEntity (..), UserMsgReceiptSettings (..), UserContactLink (..), AutoAccept (..), diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 6584aabb0a..bae9d00bfd 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -3,11 +3,13 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeOperators #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Store.Connections - ( getConnectionEntity, + ( getChatLockEntity, + getConnectionEntity, getConnectionEntityByConnReq, getContactConnEntityByConnReqHash, getConnectionsToSubscribe, @@ -20,8 +22,6 @@ import Control.Monad import Control.Monad.Except import Data.Int (Int64) import Data.Maybe (catMaybes, fromMaybe) -import Data.Text (Text) -import Data.Time.Clock (UTCTime (..)) import Database.SQLite.Simple (Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Protocol @@ -30,14 +30,37 @@ import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Types -import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (ConnId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB -import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Util (eitherToMaybe) -getConnectionEntity :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity +getChatLockEntity :: DB.Connection -> AgentConnId -> ExceptT StoreError IO ChatLockEntity +getChatLockEntity db agentConnId = do + ((connId, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId)) <- + ExceptT . firstRow id (SEConnectionNotFound agentConnId) $ + DB.query + db + [sql| + SELECT connection_id, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id + FROM connections + WHERE agent_conn_id = ? + |] + (Only agentConnId) + let err = throwError $ SEInternalError $ "connection " <> show connType <> " without entity" + case connType of + ConnMember -> maybe err (fmap CLGroup . getMemberGroupId) groupMemberId + ConnContact -> pure $ maybe (CLConnection connId) CLContact contactId + ConnSndFile -> maybe err (pure . CLFile) sndFileId + ConnRcvFile -> maybe err (pure . CLFile) rcvFileId + ConnUserContact -> maybe err (pure . CLUserContact) userContactLinkId + where + getMemberGroupId :: GroupMemberId -> ExceptT StoreError IO GroupId + getMemberGroupId groupMemberId = + ExceptT . firstRow fromOnly (SEInternalError "group member connection group_id not found") $ + DB.query db "SELECT group_id FROM group_members WHERE group_member_id = ?" (Only groupMemberId) + +getConnectionEntity :: DB.Connection -> VersionRangeChat -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do c@Connection {connType, entityId} <- getConnection_ case entityId of @@ -74,20 +97,20 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do db [sql| SELECT - c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.custom_data + c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.custom_data FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 |] (userId, contactId) - toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)] -> Either StoreError Contact - toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)] = + toContact' :: Int64 -> Connection -> [ContactRow'] -> Either StoreError Contact + toContact' contactId conn [(profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)] = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn - in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData} + in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = ExceptT $ do @@ -99,7 +122,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -158,7 +181,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId} userContact_ _ = Left SEUserContactLinkNotFound -getConnectionEntityByConnReq :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity) +getConnectionEntityByConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity) getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do connId_ <- maybeFirstRow fromOnly $ @@ -169,7 +192,7 @@ getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) -- multiple connections can have same via_contact_uri_hash if request was repeated; -- this function searches for latest connection with contact so that "known contact" plan would be chosen; -- deleted connections are filtered out to allow re-connecting via same contact address -getContactConnEntityByConnReqHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity) +getContactConnEntityByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity) getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2) = do connId_ <- maybeFirstRow fromOnly $ @@ -189,7 +212,7 @@ getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2 (userId, cReqHash1, cReqHash2, ConnDeleted) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ -getConnectionsToSubscribe :: DB.Connection -> (PQSupport -> VersionRangeChat) -> IO ([ConnId], [ConnectionEntity]) +getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> IO ([ConnId], [ConnectionEntity]) getConnectionsToSubscribe db vr = do aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1" entities <- forM aConnIds $ \acId -> do diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index ba2586b0ca..deb0f9fc4c 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -69,6 +69,7 @@ module Simplex.Chat.Store.Direct setConnConnReqInv, resetContactConnInitiated, setContactCustomData, + setContactUIThemes, ) where @@ -87,6 +88,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -126,7 +128,7 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createAddressContactConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO Contact +createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO Contact createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode chatV pqSup = do PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode chatV pqSup liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId) @@ -153,7 +155,7 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} -getConnReqContactXContactId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) +getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) getConnReqContactXContactId db vr user@User {userId} cReqHash = do getContactByConnReqHash db vr user cReqHash >>= \case c@(Just _) -> pure (c, Nothing) @@ -167,7 +169,7 @@ getConnReqContactXContactId db vr user@User {userId} cReqHash = do "SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1" (userId, cReqHash) -getContactByConnReqHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ConnReqUriHash -> IO (Maybe Contact) +getContactByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact) getContactByConnReqHash db vr user@User {userId} cReqHash = maybeFirstRow (toContact vr user) $ DB.query @@ -176,7 +178,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, @@ -222,7 +224,26 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p let profile = toLocalProfile profileId p localAlias userPreferences = emptyChatPrefs mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn - pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing} + pure $ + Contact + { contactId, + localDisplayName, + profile, + activeConn = Just conn, + viaGroup = Nothing, + contactUsed, + contactStatus = CSActive, + chatSettings = defaultChatSettings, + userPreferences, + mergedPreferences, + createdAt = currentTs, + updatedAt = currentTs, + chatTs = Just currentTs, + contactGroupMemberId = Nothing, + contactGrpInvSent = False, + customData = Nothing, + uiThemes = Nothing + } deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () deleteContactConnectionsAndFiles db userId Contact {contactId} = do @@ -279,12 +300,12 @@ setContactDeleted db user@User {userId} ct@Contact {contactId} = do currentTs <- getCurrentTime DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId) -getDeletedContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [Contact] +getDeletedContacts :: DB.Connection -> VersionRangeChat -> User -> IO [Contact] getDeletedContacts db vr user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1" (Only userId) rights <$> mapM (runExceptT . getDeletedContact db vr user) contactIds -getDeletedContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO Contact +getDeletedContact :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Contact getDeletedContact db vr user contactId = getContact_ db vr user contactId True deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO () @@ -521,18 +542,18 @@ updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt (newName, updatedAt, userId, contactId) safeDeleteLDN db user displayName -getContactByName :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactName -> ExceptT StoreError IO Contact +getContactByName :: DB.Connection -> VersionRangeChat -> User -> ContactName -> ExceptT StoreError IO Contact getContactByName db vr user localDisplayName = do cId <- getContactIdByName db user localDisplayName getContact db vr user cId -getUserContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [Contact] +getUserContacts :: DB.Connection -> VersionRangeChat -> User -> IO [Contact] getUserContacts db vr user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId) contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts -createOrUpdateContactRequest :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ContactOrRequest +createOrUpdateContactRequest :: DB.Connection -> VersionRangeChat -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ContactOrRequest createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ pqSup = liftIO (maybeM getContact' xContactId_) >>= \case Just contact -> pure $ CORContact contact @@ -579,7 +600,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, @@ -725,17 +746,36 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} contactId <- insertedRowId db conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing} + pure $ + Contact + { contactId, + localDisplayName, + profile = toLocalProfile profileId profile "", + activeConn = Just conn, + viaGroup = Nothing, + contactUsed, + contactStatus = CSActive, + chatSettings = defaultChatSettings, + userPreferences, + mergedPreferences, + createdAt, + updatedAt = createdAt, + chatTs = Just createdAt, + contactGroupMemberId = Nothing, + contactGrpInvSent = False, + uiThemes = Nothing, + customData = Nothing + } getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName db User {userId} cName = ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $ DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0" (userId, cName) -getContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO Contact +getContact :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Contact getContact db vr user contactId = getContact_ db vr user contactId False -getContact_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact +getContact_ :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact getContact_ db vr user@User {userId} contactId deleted = ExceptT . firstRow (toContact vr user) (SEContactNotFound contactId) $ DB.query @@ -744,7 +784,7 @@ getContact_ db vr user@User {userId} contactId deleted = SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, @@ -791,7 +831,7 @@ getPendingContactConnections db User {userId} = do |] [":user_id" := userId, ":conn_type" := ConnContact] -getContactConnections :: DB.Connection -> (PQSupport -> VersionRangeChat) -> UserId -> Contact -> IO [Connection] +getContactConnections :: DB.Connection -> VersionRangeChat -> UserId -> Contact -> IO [Connection] getContactConnections db vr userId Contact {contactId} = connections =<< liftIO getConnections_ where @@ -811,7 +851,7 @@ getContactConnections db vr userId Contact {contactId} = connections [] = pure [] connections rows = pure $ map (toConnection vr) rows -getConnectionById :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO Connection +getConnectionById :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Connection getConnectionById db vr User {userId} connId = ExceptT $ do firstRow (toConnection vr) (SEConnectionNotFoundById connId) $ DB.query @@ -889,3 +929,8 @@ setContactCustomData :: DB.Connection -> User -> Contact -> Maybe CustomData -> setContactCustomData db User {userId} Contact {contactId} customData = do updatedAt <- getCurrentTime DB.execute db "UPDATE contacts SET custom_data = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (customData, updatedAt, userId, contactId) + +setContactUIThemes :: DB.Connection -> User -> Contact -> Maybe UIThemeEntityOverrides -> IO () +setContactUIThemes db User {userId} Contact {contactId} uiThemes = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (uiThemes, updatedAt, userId, contactId) diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index e77681bb9b..528290aa59 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -29,8 +29,8 @@ module Simplex.Chat.Store.Files createExtraSndFTDescrs, updateSndFTDeliveryXFTP, setSndFTAgentDeleted, - getXFTPSndFileDBId, - getXFTPRcvFileDBId, + getXFTPSndFileDBIds, + getXFTPRcvFileDBIds, updateFileCancelled, updateCIFileStatus, getSharedMsgIdByFileId, @@ -109,7 +109,7 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Util (week) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) -import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) +import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) @@ -174,7 +174,7 @@ getPendingSndChunks db fileId connId = |] (fileId, connId) -createSndDirectFTConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO () +createSndDirectFTConnection :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO () createSndDirectFTConnection db vr user@User {userId} fileId (cmdId, acId) subMode = do currentTs <- getCurrentTime Connection {connId} <- createSndFileConnection_ db vr userId fileId acId subMode @@ -194,7 +194,7 @@ createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation fileId <- insertedRowId db pure FileTransferMeta {fileId, xftpSndFile = Nothing, xftpRedirectFor = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} -createSndGroupFileTransferConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO () +createSndGroupFileTransferConnection :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO () createSndGroupFileTransferConnection db vr user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} subMode = do currentTs <- getCurrentTime Connection {connId} <- createSndFileConnection_ db vr userId fileId acId subMode @@ -336,15 +336,37 @@ setSndFTAgentDeleted db User {userId} fileId = do "UPDATE files SET agent_snd_file_deleted = 1, updated_at = ? WHERE user_id = ? AND file_id = ?" (currentTs, userId, fileId) -getXFTPSndFileDBId :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO FileTransferId -getXFTPSndFileDBId db User {userId} aSndFileId = - ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $ - DB.query db "SELECT file_id FROM files WHERE user_id = ? AND agent_snd_file_id = ?" (userId, aSndFileId) +getXFTPSndFileDBIds :: DB.Connection -> AgentSndFileId -> ExceptT StoreError IO (Maybe ChatRef, FileTransferId) +getXFTPSndFileDBIds db aSndFileId = + ExceptT . firstRow' toFileRef (SESndFileNotFoundXFTP aSndFileId) $ + DB.query + db + [sql| + SELECT file_id, contact_id, group_id, note_folder_id + FROM files + WHERE agent_snd_file_id = ? + |] + (Only aSndFileId) -getXFTPRcvFileDBId :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO FileTransferId -getXFTPRcvFileDBId db aRcvFileId = - ExceptT . firstRow fromOnly (SERcvFileNotFoundXFTP aRcvFileId) $ - DB.query db "SELECT file_id FROM rcv_files WHERE agent_rcv_file_id = ?" (Only aRcvFileId) +getXFTPRcvFileDBIds :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO (Maybe ChatRef, FileTransferId) +getXFTPRcvFileDBIds db aRcvFileId = + ExceptT . firstRow' toFileRef (SERcvFileNotFoundXFTP aRcvFileId) $ + DB.query + db + [sql| + SELECT rf.file_id, f.contact_id, f.group_id, f.note_folder_id + FROM rcv_files rf + JOIN files f ON f.file_id = rf.file_id + WHERE rf.agent_rcv_file_id = ? + |] + (Only aRcvFileId) + +toFileRef :: (FileTransferId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (Maybe ChatRef, FileTransferId) +toFileRef = \case + (fileId, Just contactId, Nothing, Nothing) -> Right (Just $ ChatRef CTDirect contactId, fileId) + (fileId, Nothing, Just groupId, Nothing) -> Right (Just $ ChatRef CTGroup groupId, fileId) + (fileId, Nothing, Nothing, Just folderId) -> Right (Just $ ChatRef CTLocal folderId, fileId) + (fileId, _, _, _) -> Right (Nothing, fileId) updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () updateFileCancelled db User {userId} fileId ciFileStatus = do @@ -430,10 +452,10 @@ lookupChatRefByFileId db User {userId} fileId = (userId, fileId) -- TODO v6.0 remove -createSndFileConnection_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection +createSndFileConnection_ :: DB.Connection -> VersionRangeChat -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection createSndFileConnection_ db vr userId fileId agentConnId subMode = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId (minVersion $ vr PQSupportOff) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff + createConnection_ db userId ConnSndFile (Just fileId) agentConnId (minVersion vr) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -695,7 +717,7 @@ getRcvFileTransfer_ db userId fileId = do _ -> pure Nothing cancelled = fromMaybe False cancelled_ -acceptRcvFileTransfer :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem +acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do currentTs <- getCurrentTime acceptRcvFT_ db user fileId filePath Nothing currentTs @@ -707,7 +729,7 @@ acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus f setCommandConnId db user cmdId connId runExceptT $ getChatItemByFileId db vr user fileId -getContactByFileId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> FileTransferId -> ExceptT StoreError IO Contact +getContactByFileId :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> ExceptT StoreError IO Contact getContactByFileId db vr user@User {userId} fileId = do cId <- getContactIdByFileId getContact db vr user cId @@ -716,7 +738,7 @@ getContactByFileId db vr user@User {userId} fileId = do ExceptT . firstRow fromOnly (SEContactNotFoundByFileId fileId) $ DB.query db "SELECT contact_id FROM files WHERE user_id = ? AND file_id = ?" (userId, fileId) -acceptRcvInlineFT :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem +acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem acceptRcvInlineFT db vr user fileId filePath = do liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime getChatItemByFileId db vr user fileId @@ -725,7 +747,7 @@ startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Mayb startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline = acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime -xftpAcceptRcvFT :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem +xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem xftpAcceptRcvFT db vr user fileId filePath = do liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime getChatItemByFileId db vr user fileId @@ -1000,7 +1022,7 @@ getLocalCryptoFile db userId fileId sent = pure $ CryptoFile filePath fileCryptoArgs _ -> throwError $ SEFileNotFound fileId -updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem +updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> VersionRangeChat -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem updateDirectCIFileStatus db vr user fileId fileStatus = do aci@(AChatItem cType d cInfo ci) <- getChatItemByFileId db vr user fileId case (cType, testEquality d $ msgDirection @d) of diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 832b928012..d8655c818b 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -3,8 +3,8 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} @@ -117,6 +117,7 @@ module Simplex.Chat.Store.Groups updateUnknownMemberAnnounced, updateUserMemberProfileSentAt, setGroupCustomData, + setGroupUIThemes, ) where @@ -124,6 +125,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) +import Data.Bifunctor (second) import Data.Either (rights) import Data.Int (Int64) import Data.List (partition, sortOn) @@ -139,29 +141,31 @@ import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (PQSupport, pattern PQEncOff, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) import Simplex.Messaging.Version import UnliftIO.STM -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe CustomData) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) -toGroupInfo :: (PQSupport -> VersionRangeChat) -> Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData) :. userMemberRow) = - let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr PQSupportOff} +toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo +toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData) :. userMemberRow) = + let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} - in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData} + in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData} toGroupMember :: Int64 -> GroupMemberRow -> GroupMember toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = @@ -189,7 +193,7 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff -getGroupLinkConnection :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> ExceptT StoreError IO Connection +getGroupLinkConnection :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO Connection getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = ExceptT . firstRow (toConnection vr) (SEGroupLinkNotFound groupInfo) $ DB.query @@ -262,7 +266,7 @@ setGroupLinkMemberRole :: DB.Connection -> User -> Int64 -> GroupMemberRole -> I setGroupLinkMemberRole db User {userId} userContactLinkId memberRole = DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId) -getGroupAndMember :: DB.Connection -> User -> Int64 -> (PQSupport -> VersionRangeChat) -> ExceptT StoreError IO (GroupInfo, GroupMember) +getGroupAndMember :: DB.Connection -> User -> Int64 -> VersionRangeChat -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember db User {userId, userContactId} groupMemberId vr = ExceptT . firstRow toGroupAndMember (SEInternalError "referenced group member not found") $ DB.query @@ -272,7 +276,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -307,7 +311,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection vr connRow}) -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> (PQSupport -> VersionRangeChat) -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo +createNewGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupProfile -> Maybe Profile -> ExceptT StoreError IO GroupInfo createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = ExceptT $ do let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile fullGroupPreferences = mergeGroupPreferences groupPreferences @@ -346,11 +350,12 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc updatedAt = currentTs, chatTs = Just currentTs, userMemberProfileSentAt = Just currentTs, + uiThemes = Nothing, customData = Nothing } -- | creates a new group record for the group the current user was invited to, or returns an existing one -createGroupInvitation :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) +createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do liftIO getInvitationGroupId_ >>= \case @@ -395,7 +400,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ |] (profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) insertedRowId db - let hostVRange = const $ adjustedMemberVRange vr peerChatVRange + let hostVRange = adjustedMemberVRange vr peerChatVRange GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs vr let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} @@ -412,14 +417,15 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ updatedAt = currentTs, chatTs = Just currentTs, userMemberProfileSentAt = Just currentTs, + uiThemes = Nothing, customData = Nothing }, groupMemberId ) -adjustedMemberVRange :: (PQSupport -> VersionRangeChat) -> VersionRangeChat -> VersionRangeChat -adjustedMemberVRange getVR vr@(VersionRange minV maxV) = - let maxV' = min maxV (maxVersion $ getVR PQSupportOff) +adjustedMemberVRange :: VersionRangeChat -> VersionRangeChat -> VersionRangeChat +adjustedMemberVRange chatVR vr@(VersionRange minV maxV) = + let maxV' = min maxV (maxVersion chatVR) in fromMaybe vr $ safeVersionRange minV (max minV maxV') getHostMemberId_ :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId @@ -427,7 +433,7 @@ getHostMemberId_ db User {userId} groupId = ExceptT . firstRow fromOnly (SEHostMemberIdNotFound groupId) $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_category = ?" (userId, groupId, GCHostMember) -createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> (PQSupport -> VersionRangeChat) -> ExceptT StoreError IO GroupMember +createContactMemberInv_ :: IsContact a => DB.Connection -> User -> GroupId -> Maybe GroupMemberId -> a -> MemberIdRole -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> Maybe ProfileId -> UTCTime -> VersionRangeChat -> ExceptT StoreError IO GroupMember createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMemberId userOrContact MemberIdRole {memberId, memberRole} memberCategory memberStatus invitedBy incognitoProfileId createdAt vr = do incognitoProfile <- forM incognitoProfileId $ \profileId -> getProfileById db userId profileId (localDisplayName, memberProfile) <- case (incognitoProfile, incognitoProfileId) of @@ -455,7 +461,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe memberChatVRange } where - memberChatVRange@(VersionRange minV maxV) = vr PQSupportOff + memberChatVRange@(VersionRange minV maxV) = vr insertMember_ :: IO ContactName insertMember_ = do let localDisplayName = localDisplayName' userOrContact @@ -491,7 +497,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe ) pure $ Right incognitoLdn -createGroupInvitedViaLink :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupInvitedViaLink db vr @@ -562,7 +568,7 @@ setGroupInvitationChatItemId db User {userId} groupId chatItemId = do -- TODO return the last connection that is ready, not any last connection -- requires updating connection status -getGroup :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> ExceptT StoreError IO Group +getGroup :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO Group getGroup db vr user groupId = do gInfo <- getGroupInfo db vr user groupId members <- liftIO $ getGroupMembers db vr user gInfo @@ -617,12 +623,12 @@ deleteGroupProfile_ db userId groupId = |] (userId, groupId) -getUserGroups :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [Group] +getUserGroups :: DB.Connection -> VersionRangeChat -> User -> IO [Group] getUserGroups db vr user@User {userId} = do groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId) rights <$> mapM (runExceptT . getGroup db vr user) groupIds -getUserGroupDetails :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo] +getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo] getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = map (toGroupInfo vr userContactId) <$> DB.query @@ -631,7 +637,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences FROM groups g @@ -645,7 +651,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = where search = fromMaybe "" search_ -getUserGroupsWithSummary :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)] +getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)] getUserGroupsWithSummary db vr user _contactId_ search_ = getUserGroupDetails db vr user _contactId_ search_ >>= mapM (\g@GroupInfo {groupId} -> (g,) <$> getGroupSummary db user groupId) @@ -668,13 +674,13 @@ getGroupSummary db User {userId} groupId = do (userId, groupId, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited) pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_} -getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences] +getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [(GroupMemberRole, FullGroupPreferences)] getContactGroupPreferences db User {userId} Contact {contactId} = do - map (mergeGroupPreferences . fromOnly) + map (second mergeGroupPreferences) <$> DB.query db [sql| - SELECT gp.preferences + SELECT m.member_role, gp.preferences FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members m USING (group_id) @@ -686,7 +692,7 @@ checkContactHasGroups :: DB.Connection -> User -> Contact -> IO (Maybe GroupId) checkContactHasGroups db User {userId} Contact {contactId} = maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId) -getGroupInfoByName :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupName -> ExceptT StoreError IO GroupInfo +getGroupInfoByName :: DB.Connection -> VersionRangeChat -> User -> GroupName -> ExceptT StoreError IO GroupInfo getGroupInfoByName db vr user gName = do gId <- getGroupIdByName db user gName getGroupInfo db vr user gId @@ -710,7 +716,7 @@ groupMemberQuery = ) |] -getGroupMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember +getGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db vr user@User {userId} groupId groupMemberId = ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $ DB.query @@ -718,7 +724,7 @@ getGroupMember db vr user@User {userId} groupId groupMemberId = (groupMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?") (userId, groupId, groupMemberId, userId) -getGroupMemberById :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember +getGroupMemberById :: DB.Connection -> VersionRangeChat -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMemberById db vr user@User {userId} groupMemberId = ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $ DB.query @@ -726,7 +732,7 @@ getGroupMemberById db vr user@User {userId} groupMemberId = (groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?") (userId, groupMemberId, userId) -getGroupMemberByMemberId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember +getGroupMemberByMemberId :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember getGroupMemberByMemberId db vr user@User {userId} GroupInfo {groupId} memberId = ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByMemberId memberId) $ DB.query @@ -734,7 +740,7 @@ getGroupMemberByMemberId db vr user@User {userId} GroupInfo {groupId} memberId = (groupMemberQuery <> " WHERE m.group_id = ? AND m.member_id = ?") (userId, groupId, memberId) -getGroupMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> IO [GroupMember] +getGroupMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do map (toContactMember vr user) <$> DB.query @@ -742,7 +748,7 @@ getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do (groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)") (userId, groupId, userId, userContactId) -getGroupMembersForExpiration :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> IO [GroupMember] +getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do map (toContactMember vr user) <$> DB.query @@ -758,7 +764,7 @@ getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo { ) (userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown) -toContactMember :: (PQSupport -> VersionRangeChat) -> User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember +toContactMember :: VersionRangeChat -> User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember toContactMember vr User {userContactId} (memberRow :. connRow) = (toGroupMember userContactId memberRow) {activeConn = toMaybeConnection vr connRow} @@ -776,7 +782,7 @@ getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do (groupId, userId) pure $ length $ filter memberCurrent' statuses -getGroupInvitation :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation +getGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation getGroupInvitation db vr user groupId = getConnRec_ user >>= \case Just connRequest -> do @@ -911,7 +917,7 @@ createAcceptedMemberConnection Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff setCommandConnId db user cmdId connId -getContactViaMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> ExceptT StoreError IO Contact +getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do contactId <- ExceptT $ @@ -1172,7 +1178,7 @@ getIntroduction db reMember toMember = ExceptT $ do in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound -getForwardIntroducedMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> Bool -> IO [GroupMember] +getForwardIntroducedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember] getForwardIntroducedMembers db vr user invitee highlyAvailable = do memberIds <- map fromOnly <$> query filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds @@ -1192,7 +1198,7 @@ getForwardIntroducedMembers db vr user invitee highlyAvailable = do WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?) |] -getForwardInvitedMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> Bool -> IO [GroupMember] +getForwardInvitedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember] getForwardInvitedMembers db vr user forwardMember highlyAvailable = do memberIds <- map fromOnly <$> query filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds @@ -1286,7 +1292,7 @@ createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Version createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff -getViaGroupMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) +getViaGroupMember :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = maybeFirstRow toGroupAndMember $ DB.query @@ -1296,7 +1302,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1331,7 +1337,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = member = toGroupMember userContactId memberRow in (groupInfo, (member :: GroupMember) {activeConn = toMaybeConnection vr connRow}) -getViaGroupContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> IO (Maybe Contact) +getViaGroupContact :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> IO (Maybe Contact) getViaGroupContact db vr user@User {userId} GroupMember {groupMemberId} = do contactId_ <- maybeFirstRow fromOnly $ @@ -1382,7 +1388,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, (ldn, currentTs, userId, groupId) safeDeleteLDN db user localDisplayName -getGroupInfo :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO GroupInfo +getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo getGroupInfo db vr User {userId, userContactId} groupId = ExceptT . firstRow (toGroupInfo vr userContactId) (SEGroupNotFound groupId) $ DB.query @@ -1392,7 +1398,7 @@ getGroupInfo db vr User {userId, userContactId} groupId = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1405,7 +1411,7 @@ getGroupInfo db vr User {userId, userContactId} groupId = |] (groupId, userId, userContactId) -getGroupInfoByUserContactLinkConnReq :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) +getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do groupId_ <- maybeFirstRow fromOnly $ @@ -1419,7 +1425,7 @@ getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReq (userId, cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ -getGroupInfoByGroupLinkHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) +getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do groupId_ <- maybeFirstRow fromOnly $ @@ -1446,7 +1452,7 @@ getGroupMemberIdByName db User {userId} groupId groupMemberName = ExceptT . firstRow fromOnly (SEGroupMemberNameNotFound groupId groupMemberName) $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ?" (userId, groupId, groupMemberName) -getActiveMembersByName :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)] +getActiveMembersByName :: DB.Connection -> VersionRangeChat -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)] getActiveMembersByName db vr user@User {userId} groupMemberName = do groupMemberIds :: [(GroupId, GroupMemberId)] <- liftIO $ @@ -1467,7 +1473,7 @@ getActiveMembersByName db vr user@User {userId} groupMemberName = do where ts GroupInfo {chatTs, updatedAt} = fromMaybe updatedAt chatTs -getMatchingContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> IO [Contact] +getMatchingContacts :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO [Contact] getMatchingContacts db vr user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do contactIds <- map fromOnly <$> case image of @@ -1487,7 +1493,7 @@ getMatchingContacts db vr user@User {userId} Contact {contactId, profile = Local AND p.display_name = ? AND p.full_name = ? |] -getMatchingMembers :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> IO [GroupMember] +getMatchingMembers :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO [GroupMember] getMatchingMembers db vr user@User {userId} Contact {profile = LocalProfile {displayName, fullName, image}} = do memberIds <- map fromOnly <$> case image of @@ -1506,7 +1512,7 @@ getMatchingMembers db vr user@User {userId} Contact {profile = LocalProfile {dis AND p.display_name = ? AND p.full_name = ? |] -getMatchingMemberContacts :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> IO [Contact] +getMatchingMemberContacts :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> IO [Contact] getMatchingMemberContacts _ _ _ GroupMember {memberContactId = Just _} = pure [] getMatchingMemberContacts db vr user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} = do contactIds <- @@ -1545,7 +1551,7 @@ createSentProbeHash db userId probeId to = do "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" (probeId, ctId, gmId, userId, currentTs, currentTs) -matchReceivedProbe :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactOrMember -> Probe -> IO [ContactOrMember] +matchReceivedProbe :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> Probe -> IO [ContactOrMember] matchReceivedProbe db vr user@User {userId} from (Probe probe) = do let probeHash = C.sha256Hash probe cgmIds <- @@ -1577,7 +1583,7 @@ matchReceivedProbe db vr user@User {userId} from (Probe probe) = do (x : _) -> [x] ctIds' <> memIds -matchReceivedProbeHash :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe)) +matchReceivedProbeHash :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe)) matchReceivedProbeHash db vr user@User {userId} from (ProbeHash probeHash) = do probeIds <- maybeFirstRow id $ @@ -1600,7 +1606,7 @@ matchReceivedProbeHash db vr user@User {userId} from (ProbeHash probeHash) = do (ctId, gmId, probeHash, userId, currentTs, currentTs) pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db vr user cgmIds -matchSentProbe :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember) +matchSentProbe :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember) matchSentProbe db vr user@User {userId} _from (Probe probe) = do cgmIds $>>= getContactOrMember_ db vr user where @@ -1621,7 +1627,7 @@ matchSentProbe db vr user@User {userId} _from (Probe probe) = do |] (userId, probe, ctId, gmId) -getContactOrMember_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember) +getContactOrMember_ :: DB.Connection -> VersionRangeChat -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember) getContactOrMember_ db vr user ids = fmap eitherToMaybe . runExceptT $ case ids of (Just ctId, _, _) -> COMContact <$> getContact db vr user ctId @@ -1629,7 +1635,7 @@ getContactOrMember_ db vr user ids = _ -> throwError $ SEInternalError "" -- if requested merge direction is overruled (toFromContacts), keepLDN is kept -mergeContactRecords :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Contact -> Contact -> ExceptT StoreError IO Contact +mergeContactRecords :: DB.Connection -> VersionRangeChat -> User -> Contact -> Contact -> ExceptT StoreError IO Contact mergeContactRecords db vr user@User {userId} to@Contact {localDisplayName = keepLDN} from = do let (toCt, fromCt) = toFromContacts to from Contact {contactId = toContactId, localDisplayName = toLDN} = toCt @@ -1719,7 +1725,7 @@ associateMemberWithContactRecord when (memProfileId /= profileId) $ deleteUnusedProfile_ db userId memProfileId when (memLDN /= localDisplayName) $ deleteUnusedDisplayName_ db userId memLDN -associateContactWithMemberRecord :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact +associateContactWithMemberRecord :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact associateContactWithMemberRecord db vr @@ -1954,9 +1960,9 @@ createMemberContact authErrCounter = 0 } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, customData = Nothing} + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing} -getMemberContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) +getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact db vr user contactId = do ct <- getContact db vr user contactId let Contact {contactGroupMemberId, activeConn} = ct @@ -1991,7 +1997,7 @@ createMemberContactInvited contactId <- createContactUpdateMember currentTs userPreferences ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing} + mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing} m' = m {memberContactId = Just contactId} pure (mCt', m') where @@ -2136,7 +2142,7 @@ setXGrpLinkMemReceived db mId xGrpLinkMemReceived = do "UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?" (xGrpLinkMemReceived, currentTs, mId) -createNewUnknownGroupMember :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> MemberId -> Text -> ExceptT StoreError IO GroupMember +createNewUnknownGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Text -> ExceptT StoreError IO GroupMember createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId memberName = do currentTs <- liftIO getCurrentTime let memberProfile = profileFromName memberName @@ -2158,9 +2164,9 @@ createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {g insertedRowId db getGroupMemberById db vr user groupMemberId where - VersionRange minV maxV = vr PQSupportOff + VersionRange minV maxV = vr -updateUnknownMemberAnnounced :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupMember -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember +updateUnknownMemberAnnounced :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do _ <- updateMemberProfile db user unknownMember profile currentTs <- liftIO getCurrentTime @@ -2196,3 +2202,8 @@ setGroupCustomData :: DB.Connection -> User -> GroupInfo -> Maybe CustomData -> setGroupCustomData db User {userId} GroupInfo {groupId} customData = do updatedAt <- getCurrentTime DB.execute db "UPDATE groups SET custom_data = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (customData, updatedAt, userId, groupId) + +setGroupUIThemes :: DB.Connection -> User -> GroupInfo -> Maybe UIThemeEntityOverrides -> IO () +setGroupUIThemes db User {userId} GroupInfo {groupId} uiThemes = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE groups SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (uiThemes, updatedAt, userId, groupId) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index fe89d7f506..8163bd336c 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -145,7 +145,6 @@ import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserI import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM @@ -330,9 +329,9 @@ updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirecti (chatTs, userId, noteFolderId) _ -> pure () -createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId -createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem timed live createdAt = - createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow timed live createdAt Nothing createdAt +createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId +createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = + createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live createdAt Nothing createdAt where createdByMsgId = if msgId == 0 then Nothing else Just msgId quoteRow :: NewQuoteRow @@ -346,12 +345,13 @@ createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciCon CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv Nothing -> (Just False, Nothing) -createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c)) +createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live itemTs createdAt = do - ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow timed live itemTs forwardedByMember createdAt + ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live itemTs forwardedByMember createdAt quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg - pure (ciId, quotedItem) + pure (ciId, quotedItem, itemForwarded) where + itemForwarded = cmForwardedFrom chatMsgEvent quotedMsg = cmToQuotedMsg chatMsgEvent quoteRow :: NewQuoteRow quoteRow = case quotedMsg of @@ -364,13 +364,13 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forw createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId createNewChatItemNoMsg db user chatDirection ciContent itemTs = - createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing False itemTs Nothing + createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing Nothing False itemTs Nothing where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) -createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CITimed -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId -createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow timed live itemTs forwardedByMember createdAt = do +createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId +createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live itemTs forwardedByMember createdAt = do DB.execute db [sql| @@ -381,10 +381,12 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q item_sent, item_ts, item_content, item_content_tag, item_text, item_status, shared_msg_id, forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, -- quote - quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, + -- forwarded from + fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((userId, msgId_) :. idsRow :. itemRow :. quoteRow) + ((userId, msgId_) :. idsRow :. itemRow :. quoteRow :. forwardedFromRow) ciId <- insertedRowId db forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId @@ -399,6 +401,16 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing, Nothing) CDLocalRcv NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) CDLocalSnd NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) + forwardedFromRow :: (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) + forwardedFromRow = case itemForwarded of + Nothing -> + (Nothing, Nothing, Nothing, Nothing, Nothing, Nothing) + Just CIFFUnknown -> + (Just CIFFUnknown_, Nothing, Nothing, Nothing, Nothing, Nothing) + Just CIFFContact {chatName, msgDir, contactId, chatItemId} -> + (Just CIFFContact_, Just chatName, Just msgDir, contactId, Nothing, chatItemId) + Just CIFFGroup {chatName, msgDir, groupId, chatItemId} -> + (Just CIFFGroup_, Just chatName, Just msgDir, Nothing, groupId, chatItemId) ciTimedRow :: Maybe CITimed -> (Maybe Int, Maybe UTCTime) ciTimedRow (Just CITimed {ttl, deleteAt}) = (Just ttl, deleteAt) @@ -471,7 +483,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow -getChatPreviews :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat] +getChatPreviews :: DB.Connection -> VersionRangeChat -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat] getChatPreviews db vr user withPCC pagination query = do directChats <- findDirectChatPreviews_ db user pagination query groupChats <- findGroupChatPreviews_ db user pagination query @@ -608,7 +620,7 @@ findDirectChatPreviews_ db User {userId} pagination clq = ) ([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) -getDirectChatPreview_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat +getDirectChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat getDirectChatPreview_ db vr user (DirectChatPD _ contactId lastItemId_ stats) = do contact <- getContact db vr user contactId lastItem <- case lastItemId_ of @@ -704,7 +716,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = ) ([":user_id" := userId, ":rcv_new" := CISRcvNew, ":search" := search] <> pagParams) -getGroupChatPreview_ :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat +getGroupChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do groupInfo <- getGroupInfo db vr user groupId lastItem <- case lastItemId_ of @@ -794,7 +806,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do -- this function can be changed so it never fails, not only avoid failure on invalid json toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal) -toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = +toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -826,7 +838,8 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex DBCINotDeleted -> Nothing _ -> Just (CIDeleted @CTLocal deletedTs) itemEdited' = fromMaybe False itemEdited - in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt + itemForwarded = toCIForwardedFrom forwardedFromRow + in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} @@ -909,7 +922,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat -getDirectChat :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) getDirectChat db vr user contactId pagination search_ = do let search = fromMaybe "" search_ ct <- getContact db vr user contactId @@ -1029,7 +1042,7 @@ getDirectChatBefore_ db user@User {userId} ct@Contact {contactId} beforeChatItem |] (userId, contactId, search, beforeChatItemId, count) -getGroupChat :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup) getGroupChat db vr user groupId pagination search_ = do let search = fromMaybe "" search_ g <- getGroupInfo db vr user groupId @@ -1391,7 +1404,14 @@ type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool) -type ChatItemRow = (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) :. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) :. ChatItemModeRow :. MaybeCIFIleRow +type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) + +type ChatItemRow = + (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) + :. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) + :. ChatItemForwardedFromRow + :. ChatItemModeRow + :. MaybeCIFIleRow type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool) @@ -1406,7 +1426,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir -- this function can be changed so it never fails, not only avoid failure on invalid json toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect) -toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = +toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -1438,10 +1458,19 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT DBCINotDeleted -> Nothing _ -> Just (CIDeleted @CTDirect deletedTs) itemEdited' = fromMaybe False itemEdited - in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt + itemForwarded = toCIForwardedFrom forwardedFromRow + in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} +toCIForwardedFrom :: ChatItemForwardedFromRow -> Maybe CIForwardedFrom +toCIForwardedFrom (fwdFromTag, fwdFromChatName, fwdFromMsgDir, fwdFromContactId, fwdFromGroupId, fwdFromChatItemId) = + case (fwdFromTag, fwdFromChatName, fwdFromMsgDir, fwdFromContactId, fwdFromGroupId, fwdFromChatItemId) of + (Just CIFFUnknown_, Nothing, Nothing, Nothing, Nothing, Nothing) -> Just CIFFUnknown + (Just CIFFContact_, Just chatName, Just msgDir, contactId, Nothing, ciId) -> Just $ CIFFContact chatName msgDir contactId ciId + (Just CIFFGroup_, Just chatName, Just msgDir, Nothing, groupId, ciId) -> Just $ CIFFGroup chatName msgDir groupId ciId + _ -> Nothing + type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow toGroupQuote :: QuoteRow -> Maybe GroupMember -> Maybe (CIQuote 'CTGroup) @@ -1454,7 +1483,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction -- this function can be changed so it never fails, not only avoid failure on invalid json toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) -toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do +toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do chatItem $ fromRight invalid $ dbParseACIContent itemContentText where member_ = toMaybeGroupMember userContactId memberRow_ @@ -1491,11 +1520,12 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs) _ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) itemEdited' = fromMaybe False itemEdited - in mkCIMeta itemId content itemText status sharedMsgId itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt + itemForwarded = toCIForwardedFrom forwardedFromRow + in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} -getAllChatItems :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem] +getAllChatItems :: DB.Connection -> VersionRangeChat -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem] getAllChatItems db vr user@User {userId} pagination search_ = do itemRefs <- rights . map toChatItemRef <$> case pagination of @@ -1570,10 +1600,10 @@ updateDirectChatItemStatus db user@User {userId} ct@Contact {contactId} itemId i liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId) pure ci {meta = (meta ci) {itemStatus}} -updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) -updateDirectChatItem db user ct@Contact {contactId} itemId newContent live msgId_ = do +updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItem db user ct@Contact {contactId} itemId newContent edited live timed_ msgId_ = do ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId - liftIO $ updateDirectChatItem' db user contactId ci newContent live msgId_ + liftIO $ updateDirectChatItem' db user contactId ci newContent edited live timed_ msgId_ getDirectCIWithReactions :: DB.Connection -> User -> Contact -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectCIWithReactions db user ct@Contact {contactId} itemId = @@ -1582,25 +1612,27 @@ getDirectCIWithReactions db user ct@Contact {contactId} itemId = correctDir :: MsgDirectionI d => CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d) -updateDirectChatItem' db User {userId} contactId ci newContent live msgId_ = do +updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> IO (ChatItem 'CTDirect d) +updateDirectChatItem' db User {userId} contactId ci newContent edited live timed_ msgId_ = do currentTs <- liftIO getCurrentTime - let ci' = updatedChatItem ci newContent live currentTs + let ci' = updatedChatItem ci newContent edited live timed_ currentTs liftIO $ updateDirectChatItem_ db userId contactId ci' msgId_ pure ci' -updatedChatItem :: ChatItem c d -> CIContent d -> Bool -> UTCTime -> ChatItem c d -updatedChatItem ci@ChatItem {meta = meta@CIMeta {itemStatus, itemEdited, itemTimed, itemLive}} newContent live currentTs = +updatedChatItem :: ChatItem c d -> CIContent d -> Bool -> Bool -> Maybe CITimed -> UTCTime -> ChatItem c d +updatedChatItem ci@ChatItem {meta = meta@CIMeta {itemStatus, itemEdited, itemTimed, itemLive}} newContent edited live timed_ currentTs = let newText = ciContentToText newContent - edited' = itemEdited || (itemLive /= Just True) + edited' = itemEdited || edited live' = (live &&) <$> itemLive - timed' = case (itemStatus, itemTimed, itemLive, live) of - (CISRcvNew, _, _, _) -> itemTimed - (_, Just CITimed {ttl, deleteAt = Nothing}, Just True, False) -> - -- timed item, sent or read, not set for deletion, was live, now not live - let deleteAt' = addUTCTime (realToFrac ttl) currentTs - in Just CITimed {ttl, deleteAt = Just deleteAt'} - _ -> itemTimed + timed' = case timed_ of + Just timed -> Just timed + Nothing -> case (itemStatus, itemTimed, itemLive, live) of + (CISRcvNew, _, _, _) -> itemTimed + (_, Just CITimed {ttl, deleteAt = Nothing}, Just True, False) -> + -- timed item, sent or read, not set for deletion, was live, now not live + let deleteAt' = addUTCTime (realToFrac ttl) currentTs + in Just CITimed {ttl, deleteAt = Just deleteAt'} + _ -> itemTimed in ci {content = newContent, meta = meta {itemText = newText, itemEdited = edited', itemTimed = timed', itemLive = live'}, formattedText = parseMaybeMarkdownList newText} -- this function assumes that direct item with correct chat direction already exists, @@ -1726,7 +1758,10 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote @@ -1785,10 +1820,10 @@ groupCIWithReactions db g cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemShar pure $ CChatItem md ci {reactions} Nothing -> pure cci -updateGroupChatItem :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d) -updateGroupChatItem db user groupId ci newContent live msgId_ = do +updateGroupChatItem :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d) +updateGroupChatItem db user groupId ci newContent edited live msgId_ = do currentTs <- liftIO getCurrentTime - let ci' = updatedChatItem ci newContent live currentTs + let ci' = updatedChatItem ci newContent edited live Nothing currentTs liftIO $ updateGroupChatItem_ db user groupId ci' msgId_ pure ci' @@ -1840,7 +1875,7 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId) - pure $ ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False}, formattedText = Nothing} + pure $ ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} updateGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> UTCTime -> IO (ChatItem 'CTGroup d) updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = do @@ -1857,7 +1892,7 @@ updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIBlockedByAdmin, deletedTs, currentTs, userId, groupId, itemId) - pure $ ci {meta = (meta ci) {itemDeleted = Just (CIBlockedByAdmin $ Just deletedTs), editable = False}, formattedText = Nothing} + pure $ ci {meta = (meta ci) {itemDeleted = Just (CIBlockedByAdmin $ Just deletedTs), editable = False, deletable = False}, formattedText = Nothing} pattern DBCINotDeleted :: Int pattern DBCINotDeleted = 0 @@ -1966,7 +2001,10 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- CIMeta forwardedByMember @@ -2067,7 +2105,10 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, + i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, + i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, + i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol FROM chat_items i @@ -2104,10 +2145,10 @@ getLocalChatItemIdByText' db User {userId} noteFolderId msg = |] (userId, noteFolderId, msg <> "%") -updateLocalChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> NoteFolderId -> ChatItem 'CTLocal d -> CIContent d -> IO (ChatItem 'CTLocal d) -updateLocalChatItem' db User {userId} noteFolderId ci newContent = do +updateLocalChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> NoteFolderId -> ChatItem 'CTLocal d -> CIContent d -> Bool -> IO (ChatItem 'CTLocal d) +updateLocalChatItem' db User {userId} noteFolderId ci newContent edited = do currentTs <- liftIO getCurrentTime - let ci' = updatedChatItem ci newContent False currentTs + let ci' = updatedChatItem ci newContent edited False Nothing currentTs liftIO $ updateLocalChatItem_ db userId noteFolderId ci' pure ci' @@ -2139,7 +2180,7 @@ deleteLocalChatItem db User {userId} NoteFolder {noteFolderId} ci = do |] (userId, noteFolderId, itemId) -getChatItemByFileId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO AChatItem +getChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO AChatItem getChatItemByFileId db vr user@User {userId} fileId = do (chatRef, itemId) <- ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByFileId fileId) $ @@ -2155,13 +2196,13 @@ getChatItemByFileId db vr user@User {userId} fileId = do (userId, fileId) getAChatItem db vr user chatRef itemId -lookupChatItemByFileId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> Int64 -> ExceptT StoreError IO (Maybe AChatItem) +lookupChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO (Maybe AChatItem) lookupChatItemByFileId db vr user fileId = do fmap Just (getChatItemByFileId db vr user fileId) `catchError` \case SEChatItemNotFoundByFileId {} -> pure Nothing e -> throwError e -getChatItemByGroupId :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupId -> ExceptT StoreError IO AChatItem +getChatItemByGroupId :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO AChatItem getChatItemByGroupId db vr user@User {userId} groupId = do (chatRef, itemId) <- ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByGroupId groupId) $ @@ -2187,7 +2228,7 @@ getChatRefViaItemId db User {userId} itemId = do (Nothing, Just groupId) -> Right $ ChatRef CTGroup groupId (_, _) -> Left $ SEBadChatItem itemId Nothing -getAChatItem :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem +getAChatItem :: DB.Connection -> VersionRangeChat -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem getAChatItem db vr user chatRef itemId = case chatRef of ChatRef CTDirect contactId -> do ct <- getContact db vr user contactId @@ -2436,7 +2477,7 @@ createCIModeration db GroupInfo {groupId} moderatorMember itemMemberId itemShare |] (groupId, groupMemberId' moderatorMember, itemMemberId, itemSharedMId, msgId, moderatedAtTs) -getCIModeration :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration) +getCIModeration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration) getCIModeration _ _ _ _ _ Nothing = pure Nothing getCIModeration db vr user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do r_ <- diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index e351f0f27a..2b44778272 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -104,6 +104,8 @@ import Simplex.Chat.Migrations.M20240226_users_restrict import Simplex.Chat.Migrations.M20240228_pq import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id import Simplex.Chat.Migrations.M20240324_custom_data +import Simplex.Chat.Migrations.M20240402_item_forwarded +import Simplex.Chat.Migrations.M20240430_ui_theme import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -207,7 +209,9 @@ schemaMigrations = ("20240226_users_restrict", m20240226_users_restrict, Just down_m20240226_users_restrict), ("20240228_pq", m20240228_pq, Just down_m20240228_pq), ("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id), - ("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data) + ("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data), + ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded), + ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 512c857b23..9b83e7299f 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -57,6 +57,7 @@ module Simplex.Chat.Store.Profiles deleteCommand, updateCommandStatus, getCommandDataByCorrId, + setUserUIThemes, ) where @@ -81,12 +82,13 @@ import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode) @@ -123,7 +125,7 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image, (profileId, displayName, userId, True, currentTs, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing, Nothing) + pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing, Nothing, Nothing) getUsersInfo :: DB.Connection -> IO [UserInfo] getUsersInfo db = getUsers db >>= mapM getUserInfo @@ -274,8 +276,8 @@ updateUserProfile db user p' where updateUserMemberProfileUpdatedAt_ currentTs | userMemberProfileChanged = do - DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId) - pure $ Just currentTs + DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId) + pure $ Just currentTs | otherwise = pure userMemberProfileUpdatedAt userMemberProfileChanged = newName /= displayName || newFullName /= fullName || newImage /= image User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, image, localAlias}, userMemberProfileUpdatedAt} = user @@ -327,7 +329,7 @@ createUserContactLink db User {userId} agentConnId cReq subMode = userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff -getUserAddressConnections :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ExceptT StoreError IO [Connection] +getUserAddressConnections :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO [Connection] getUserAddressConnections db vr User {userId} = do cs <- liftIO getUserAddressConnections_ if null cs then throwError SEUserContactLinkNotFound else pure cs @@ -348,7 +350,7 @@ getUserAddressConnections db vr User {userId} = do |] (userId, userId) -getUserContactLinks :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> IO [(Connection, UserContact)] +getUserContactLinks :: DB.Connection -> VersionRangeChat -> User -> IO [(Connection, UserContact)] getUserContactLinks db vr User {userId} = map toUserContactConnection <$> DB.query @@ -474,7 +476,7 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = |] (userId, cReqSchema1, cReqSchema2) -getContactWithoutConnViaAddress :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) +getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do ctId_ <- maybeFirstRow fromOnly $ @@ -619,3 +621,8 @@ getCommandDataByCorrId db User {userId} corrId = where toCommandData :: (CommandId, Maybe Int64, CommandFunction, CommandStatus) -> CommandData toCommandData (cmdId, cmdConnId, cmdFunction, cmdStatus) = CommandData {cmdId, cmdConnId, cmdFunction, cmdStatus} + +setUserUIThemes :: DB.Connection -> User -> Maybe UIThemeEntityOverrides -> IO () +setUserUIThemes db User {userId} uiThemes = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE users SET ui_themes = ?, updated_at = ? WHERE user_id = ?" (uiThemes, updatedAt, userId) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 88540134fe..1f16ebfef0 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -3,8 +3,8 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-} @@ -33,6 +33,7 @@ import Simplex.Chat.Protocol import Simplex.Chat.Remote.Types import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -45,6 +46,15 @@ import Simplex.Messaging.Util (allFinally) import Simplex.Messaging.Version import UnliftIO.STM +data ChatLockEntity + = CLInvitation ByteString + | CLConnection Int64 + | CLContact ContactId + | CLGroup GroupId + | CLUserContact Int64 + | CLFile Int64 + deriving (Eq, Ord) + -- These error type constructors must be added to mobile apps data StoreError = SEDuplicateName @@ -157,12 +167,12 @@ type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) -toConnection :: (PQSupport -> VersionRangeChat) -> ConnectionRow -> Connection +toConnection :: VersionRangeChat -> ConnectionRow -> Connection toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, chatV, minVer, maxVer)) = Connection { connId, agentConnId = AgentConnId acId, - connChatVersion = fromMaybe (vr pqSupport `peerConnChatVersion` peerChatVRange) chatV, + connChatVersion = fromMaybe (vr `peerConnChatVersion` peerChatVRange) chatV, peerChatVRange = peerChatVRange, connLevel, viaContact, @@ -192,7 +202,7 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGr entityId_ ConnSndFile = sndFileId entityId_ ConnUserContact = userContactLinkId -toMaybeConnection :: (PQSupport -> VersionRangeChat) -> MaybeConnectionRow -> Maybe Connection +toMaybeConnection :: VersionRangeChat -> MaybeConnectionRow -> Maybe Connection toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, connChatVersion, Just minVer, Just maxVer)) = Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, connChatVersion, minVer, maxVer)) toMaybeConnection _ _ = Nothing @@ -371,16 +381,18 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId = |] [":user_id" := userId, ":profile_id" := profileId] -type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData) +type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe UIThemeEntityOverrides, Maybe CustomData) -toContact :: (PQSupport -> VersionRangeChat) -> User -> ContactRow :. MaybeConnectionRow -> Contact -toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)) :. connRow) = +type ContactRow = Only ContactId :. ContactRow' + +toContact :: VersionRangeChat -> User -> ContactRow :. MaybeConnectionRow -> Contact +toContact vr user ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} activeConn = toMaybeConnection vr connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData} getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById db userId profileId = @@ -409,15 +421,15 @@ userQuery :: Query userQuery = [sql| SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (Bool, Bool, Bool, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime) -> User -toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt)) = - User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt} +toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (Bool, Bool, Bool, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User +toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) = + User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes} where profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences = userPreferences, localAlias = ""} fullPreferences = mergePreferences Nothing userPreferences diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 7b96abc1ce..5c36994190 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -86,7 +86,10 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do Right SendLiveMessage {} -> True Right SendFile {} -> True Right SendMessageQuote {} -> True + Right ForwardMessage {} -> True + Right ForwardLocalMessage {} -> True Right SendGroupMessageQuote {} -> True + Right ForwardGroupMessage {} -> True Right SendMessageBroadcast {} -> True _ -> False startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO () diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e419f8c4cb..0d07d5e3cb 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -30,7 +30,6 @@ import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString, pack, unpack) -import qualified Data.ByteString.Char8 as B import Data.Int (Int64) import Data.Maybe (isJust) import Data.Text (Text) @@ -45,6 +44,8 @@ import Database.SQLite.Simple.Internal (Field (..)) import Database.SQLite.Simple.Ok import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId) @@ -117,7 +118,8 @@ data User = User showNtfs :: Bool, sendRcptsContacts :: Bool, sendRcptsSmallGroups :: Bool, - userMemberProfileUpdatedAt :: Maybe UTCTime + userMemberProfileUpdatedAt :: Maybe UTCTime, + uiThemes :: Maybe UIThemeEntityOverrides } deriving (Show) @@ -175,6 +177,7 @@ data Contact = Contact chatTs :: Maybe UTCTime, contactGroupMemberId :: Maybe GroupMemberId, contactGrpInvSent :: Bool, + uiThemes :: Maybe UIThemeEntityOverrides, customData :: Maybe CustomData } deriving (Eq, Show) @@ -372,6 +375,7 @@ data GroupInfo = GroupInfo updatedAt :: UTCTime, chatTs :: Maybe UTCTime, userMemberProfileSentAt :: Maybe UTCTime, + uiThemes :: Maybe UIThemeEntityOverrides, customData :: Maybe CustomData } deriving (Eq, Show) @@ -439,9 +443,13 @@ featureAllowed feature forWhom Contact {mergedPreferences} = let ContactUserPreference {enabled} = getContactUserPreference feature mergedPreferences in forWhom enabled -groupFeatureAllowed :: GroupFeatureI f => SGroupFeature f -> GroupInfo -> Bool +groupFeatureAllowed :: GroupFeatureNoRoleI f => SGroupFeature f -> GroupInfo -> Bool groupFeatureAllowed feature gInfo = groupFeatureAllowed' feature $ fullGroupPreferences gInfo +groupFeatureMemberAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupMember -> GroupInfo -> Bool +groupFeatureMemberAllowed feature GroupMember {memberRole} = + groupFeatureMemberAllowed' feature memberRole . fullGroupPreferences + mergeUserChatPrefs :: User -> Contact -> FullPreferences mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) @@ -796,41 +804,6 @@ fromInvitedBy userCtId = \case IBContact ctId -> Just ctId IBUser -> Just userCtId -data GroupMemberRole - = GRObserver -- connects to all group members and receives all messages, can't send messages - | GRAuthor -- reserved, unused - | GRMember -- + can send messages to all group members - | GRAdmin -- + add/remove members, change member role (excl. Owners) - | GROwner -- + delete and change group information, add/remove/change roles for Owners - deriving (Eq, Show, Ord) - -instance FromField GroupMemberRole where fromField = fromBlobField_ strDecode - -instance ToField GroupMemberRole where toField = toField . strEncode - -instance StrEncoding GroupMemberRole where - strEncode = \case - GROwner -> "owner" - GRAdmin -> "admin" - GRMember -> "member" - GRAuthor -> "author" - GRObserver -> "observer" - strDecode = \case - "owner" -> Right GROwner - "admin" -> Right GRAdmin - "member" -> Right GRMember - "author" -> Right GRAuthor - "observer" -> Right GRObserver - r -> Left $ "bad GroupMemberRole " <> B.unpack r - strP = strDecode <$?> A.takeByteString - -instance FromJSON GroupMemberRole where - parseJSON = strParseJSON "GroupMemberRole" - -instance ToJSON GroupMemberRole where - toJSON = strToJSON - toEncoding = strToJEncoding - data GroupMemberSettings = GroupMemberSettings { showMessages :: Bool } diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index 2286ae8f40..4cf9f862d2 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -10,6 +10,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilyDependencies #-} @@ -31,6 +32,7 @@ import qualified Data.Text as T import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Records.Compat +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON) @@ -148,6 +150,7 @@ data GroupFeature | GFReactions | GFVoice | GFFiles + | GFSimplexLinks | GFHistory deriving (Show) @@ -158,14 +161,23 @@ data SGroupFeature (f :: GroupFeature) where SGFReactions :: SGroupFeature 'GFReactions SGFVoice :: SGroupFeature 'GFVoice SGFFiles :: SGroupFeature 'GFFiles + SGFSimplexLinks :: SGroupFeature 'GFSimplexLinks SGFHistory :: SGroupFeature 'GFHistory deriving instance Show (SGroupFeature f) data AGroupFeature = forall f. GroupFeatureI f => AGF (SGroupFeature f) +data AGroupFeatureNoRole = forall f. GroupFeatureNoRoleI f => AGFNR (SGroupFeature f) + +data AGroupFeatureRole = forall f. GroupFeatureRoleI f => AGFR (SGroupFeature f) + deriving instance Show AGroupFeature +deriving instance Show AGroupFeatureNoRole + +deriving instance Show AGroupFeatureRole + groupFeatureNameText :: GroupFeature -> Text groupFeatureNameText = \case GFTimedMessages -> "Disappearing messages" @@ -174,15 +186,21 @@ groupFeatureNameText = \case GFReactions -> "Message reactions" GFVoice -> "Voice messages" GFFiles -> "Files and media" + GFSimplexLinks -> "SimpleX links" GFHistory -> "Recent history" groupFeatureNameText' :: SGroupFeature f -> Text groupFeatureNameText' = groupFeatureNameText . toGroupFeature -groupFeatureAllowed' :: GroupFeatureI f => SGroupFeature f -> FullGroupPreferences -> Bool +groupFeatureAllowed' :: GroupFeatureNoRoleI f => SGroupFeature f -> FullGroupPreferences -> Bool groupFeatureAllowed' feature prefs = getField @"enable" (getGroupPreference feature prefs) == FEOn +groupFeatureMemberAllowed' :: GroupFeatureRoleI f => SGroupFeature f -> GroupMemberRole -> FullGroupPreferences -> Bool +groupFeatureMemberAllowed' feature role prefs = + let pref = getGroupPreference feature prefs + in getField @"enable" pref == FEOn && maybe True (role >=) (getField @"role" pref) + allGroupFeatures :: [AGroupFeature] allGroupFeatures = [ AGF SGFTimedMessages, @@ -191,17 +209,19 @@ allGroupFeatures = AGF SGFReactions, AGF SGFVoice, AGF SGFFiles, + AGF SGFSimplexLinks, AGF SGFHistory ] groupPrefSel :: SGroupFeature f -> GroupPreferences -> Maybe (GroupFeaturePreference f) -groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, history} = case f of +groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, history} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete SGFReactions -> reactions SGFVoice -> voice SGFFiles -> files + SGFSimplexLinks -> simplexLinks SGFHistory -> history toGroupFeature :: SGroupFeature f -> GroupFeature @@ -212,6 +232,7 @@ toGroupFeature = \case SGFReactions -> GFReactions SGFVoice -> GFVoice SGFFiles -> GFFiles + SGFSimplexLinks -> GFSimplexLinks SGFHistory -> GFHistory class GroupPreferenceI p where @@ -224,13 +245,14 @@ instance GroupPreferenceI (Maybe GroupPreferences) where getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs) instance GroupPreferenceI FullGroupPreferences where - getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, history} = case f of + getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, history} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete SGFReactions -> reactions SGFVoice -> voice SGFFiles -> files + SGFSimplexLinks -> simplexLinks SGFHistory -> history {-# INLINE getGroupPreference #-} @@ -242,17 +264,25 @@ data GroupPreferences = GroupPreferences reactions :: Maybe ReactionsGroupPreference, voice :: Maybe VoiceGroupPreference, files :: Maybe FilesGroupPreference, + simplexLinks :: Maybe SimplexLinksGroupPreference, history :: Maybe HistoryGroupPreference } deriving (Eq, Show) -setGroupPreference :: forall f. GroupFeatureI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences +setGroupPreference :: forall f. GroupFeatureNoRoleI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences setGroupPreference f enable prefs_ = setGroupPreference_ f pref prefs where prefs = mergeGroupPreferences prefs_ pref :: GroupFeaturePreference f pref = setField @"enable" (getGroupPreference f prefs) enable +setGroupPreferenceRole :: forall f. GroupFeatureRoleI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupMemberRole -> Maybe GroupPreferences -> GroupPreferences +setGroupPreferenceRole f enable role prefs_ = setGroupPreference_ f pref prefs + where + prefs = mergeGroupPreferences prefs_ + pref :: GroupFeaturePreference f + pref = setField @"role" (setField @"enable" (getGroupPreference f prefs) enable) role + setGroupPreference' :: SGroupFeature f -> GroupFeaturePreference f -> Maybe GroupPreferences -> GroupPreferences setGroupPreference' f pref prefs_ = setGroupPreference_ f pref prefs where @@ -267,6 +297,7 @@ setGroupPreference_ f pref prefs = SGFReactions -> prefs {reactions = pref} SGFVoice -> prefs {voice = pref} SGFFiles -> prefs {files = pref} + SGFSimplexLinks -> prefs {simplexLinks = pref} SGFHistory -> prefs {history = pref} setGroupTimedMessagesPreference :: TimedMessagesGroupPreference -> Maybe GroupPreferences -> GroupPreferences @@ -295,6 +326,7 @@ data FullGroupPreferences = FullGroupPreferences reactions :: ReactionsGroupPreference, voice :: VoiceGroupPreference, files :: FilesGroupPreference, + simplexLinks :: SimplexLinksGroupPreference, history :: HistoryGroupPreference } deriving (Eq, Show) @@ -346,16 +378,17 @@ defaultGroupPrefs :: FullGroupPreferences defaultGroupPrefs = FullGroupPreferences { timedMessages = TimedMessagesGroupPreference {enable = FEOff, ttl = Just 86400}, - directMessages = DirectMessagesGroupPreference {enable = FEOff}, + directMessages = DirectMessagesGroupPreference {enable = FEOff, role = Nothing}, fullDelete = FullDeleteGroupPreference {enable = FEOff}, reactions = ReactionsGroupPreference {enable = FEOn}, - voice = VoiceGroupPreference {enable = FEOn}, - files = FilesGroupPreference {enable = FEOn}, + voice = VoiceGroupPreference {enable = FEOn, role = Nothing}, + files = FilesGroupPreference {enable = FEOn, role = Nothing}, + simplexLinks = SimplexLinksGroupPreference {enable = FEOn, role = Nothing}, history = HistoryGroupPreference {enable = FEOff} } emptyGroupPrefs :: GroupPreferences -emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing +emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing data TimedMessagesPreference = TimedMessagesPreference { allow :: FeatureAllowed, @@ -431,7 +464,7 @@ data TimedMessagesGroupPreference = TimedMessagesGroupPreference deriving (Eq, Show) data DirectMessagesGroupPreference = DirectMessagesGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data FullDeleteGroupPreference = FullDeleteGroupPreference @@ -443,11 +476,15 @@ data ReactionsGroupPreference = ReactionsGroupPreference deriving (Eq, Show) data VoiceGroupPreference = VoiceGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data FilesGroupPreference = FilesGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} + deriving (Eq, Show) + +data SimplexLinksGroupPreference = SimplexLinksGroupPreference + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data HistoryGroupPreference = HistoryGroupPreference @@ -458,6 +495,11 @@ class (Eq (GroupFeaturePreference f), HasField "enable" (GroupFeaturePreference type GroupFeaturePreference (f :: GroupFeature) = p | p -> f sGroupFeature :: SGroupFeature f groupPrefParam :: GroupFeaturePreference f -> Maybe Int + groupPrefRole :: GroupFeaturePreference f -> Maybe GroupMemberRole + +class GroupFeatureI f => GroupFeatureNoRoleI f + +class (GroupFeatureI f, HasField "role" (GroupFeaturePreference f) (Maybe GroupMemberRole)) => GroupFeatureRoleI f instance HasField "enable" GroupPreference GroupFeatureEnabled where hasField p@GroupPreference {enable} = (\e -> p {enable = e}, enable) @@ -480,6 +522,9 @@ instance HasField "enable" VoiceGroupPreference GroupFeatureEnabled where instance HasField "enable" FilesGroupPreference GroupFeatureEnabled where hasField p@FilesGroupPreference {enable} = (\e -> p {enable = e}, enable) +instance HasField "enable" SimplexLinksGroupPreference GroupFeatureEnabled where + hasField p@SimplexLinksGroupPreference {enable} = (\e -> p {enable = e}, enable) + instance HasField "enable" HistoryGroupPreference GroupFeatureEnabled where hasField p@HistoryGroupPreference {enable} = (\e -> p {enable = e}, enable) @@ -487,42 +532,84 @@ instance GroupFeatureI 'GFTimedMessages where type GroupFeaturePreference 'GFTimedMessages = TimedMessagesGroupPreference sGroupFeature = SGFTimedMessages groupPrefParam TimedMessagesGroupPreference {ttl} = ttl + groupPrefRole _ = Nothing instance GroupFeatureI 'GFDirectMessages where type GroupFeaturePreference 'GFDirectMessages = DirectMessagesGroupPreference sGroupFeature = SGFDirectMessages groupPrefParam _ = Nothing + groupPrefRole DirectMessagesGroupPreference {role} = role instance GroupFeatureI 'GFFullDelete where type GroupFeaturePreference 'GFFullDelete = FullDeleteGroupPreference sGroupFeature = SGFFullDelete groupPrefParam _ = Nothing + groupPrefRole _ = Nothing instance GroupFeatureI 'GFReactions where type GroupFeaturePreference 'GFReactions = ReactionsGroupPreference sGroupFeature = SGFReactions groupPrefParam _ = Nothing + groupPrefRole _ = Nothing instance GroupFeatureI 'GFVoice where type GroupFeaturePreference 'GFVoice = VoiceGroupPreference sGroupFeature = SGFVoice groupPrefParam _ = Nothing + groupPrefRole VoiceGroupPreference {role} = role instance GroupFeatureI 'GFFiles where type GroupFeaturePreference 'GFFiles = FilesGroupPreference sGroupFeature = SGFFiles groupPrefParam _ = Nothing + groupPrefRole FilesGroupPreference {role} = role + +instance GroupFeatureI 'GFSimplexLinks where + type GroupFeaturePreference 'GFSimplexLinks = SimplexLinksGroupPreference + sGroupFeature = SGFSimplexLinks + groupPrefParam _ = Nothing + groupPrefRole SimplexLinksGroupPreference {role} = role instance GroupFeatureI 'GFHistory where type GroupFeaturePreference 'GFHistory = HistoryGroupPreference sGroupFeature = SGFHistory groupPrefParam _ = Nothing + groupPrefRole _ = Nothing -groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> p -> Maybe Int -> Text -groupPrefStateText feature pref param = +instance GroupFeatureNoRoleI 'GFTimedMessages + +instance GroupFeatureNoRoleI 'GFFullDelete + +instance GroupFeatureNoRoleI 'GFReactions + +instance GroupFeatureNoRoleI 'GFHistory + +instance HasField "role" DirectMessagesGroupPreference (Maybe GroupMemberRole) where + hasField p@DirectMessagesGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" VoiceGroupPreference (Maybe GroupMemberRole) where + hasField p@VoiceGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" FilesGroupPreference (Maybe GroupMemberRole) where + hasField p@FilesGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" SimplexLinksGroupPreference (Maybe GroupMemberRole) where + hasField p@SimplexLinksGroupPreference {role} = (\r -> p {role = r}, role) + +instance GroupFeatureRoleI 'GFDirectMessages + +instance GroupFeatureRoleI 'GFVoice + +instance GroupFeatureRoleI 'GFFiles + +instance GroupFeatureRoleI 'GFSimplexLinks + +groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> p -> Maybe Int -> Maybe GroupMemberRole -> Text +groupPrefStateText feature pref param role = let enabled = getField @"enable" pref paramText = if enabled == FEOn then groupParamText_ feature param else "" - in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText + roleText = maybe "" (\r -> " for " <> safeDecodeUtf8 (strEncode r) <> "s") role + in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText <> roleText groupParamText_ :: GroupFeature -> Maybe Int -> Text groupParamText_ feature param = case feature of @@ -532,7 +619,7 @@ groupParamText_ feature param = case feature of groupPreferenceText :: forall f. GroupFeatureI f => GroupFeaturePreference f -> Text groupPreferenceText pref = let feature = toGroupFeature $ sGroupFeature @f - in groupPrefStateText feature pref $ groupPrefParam pref + in groupPrefStateText feature pref (groupPrefParam pref) (groupPrefRole pref) timedTTLText :: Int -> Text timedTTLText 0 = "0 sec" @@ -602,7 +689,7 @@ instance StrEncoding GroupFeatureEnabled where "on" -> Right FEOn "off" -> Right FEOff r -> Left $ "bad GroupFeatureEnabled " <> B.unpack r - strP = strDecode <$?> A.takeByteString + strP = strDecode <$?> A.takeTill (== ' ') instance FromJSON GroupFeatureEnabled where parseJSON = strParseJSON "GroupFeatureEnabled" @@ -611,11 +698,13 @@ instance ToJSON GroupFeatureEnabled where toJSON = strToJSON toEncoding = strToJEncoding -groupFeatureState :: GroupFeatureI f => GroupFeaturePreference f -> (GroupFeatureEnabled, Maybe Int) +groupFeatureState :: GroupFeatureI f => GroupFeaturePreference f -> (GroupFeatureEnabled, Maybe Int, Maybe GroupMemberRole) groupFeatureState p = let enable = getField @"enable" p - param = if enable == FEOn then groupPrefParam p else Nothing - in (enable, param) + (param, role) + | enable == FEOn = (groupPrefParam p, groupPrefRole p) + | otherwise = (Nothing, Nothing) + in (enable, param, role) mergePreferences :: Maybe Preferences -> Maybe Preferences -> FullPreferences mergePreferences contactPrefs userPreferences = @@ -641,6 +730,7 @@ mergeGroupPreferences groupPreferences = reactions = pref SGFReactions, voice = pref SGFVoice, files = pref SGFFiles, + simplexLinks = pref SGFSimplexLinks, history = pref SGFHistory } where @@ -656,6 +746,7 @@ toGroupPreferences groupPreferences = reactions = pref SGFReactions, voice = pref SGFVoice, files = pref SGFFiles, + simplexLinks = pref SGFSimplexLinks, history = pref SGFHistory } where @@ -762,6 +853,8 @@ $(J.deriveJSON defaultJSON ''VoiceGroupPreference) $(J.deriveJSON defaultJSON ''FilesGroupPreference) +$(J.deriveJSON defaultJSON ''SimplexLinksGroupPreference) + $(J.deriveJSON defaultJSON ''HistoryGroupPreference) $(J.deriveJSON defaultJSON ''GroupPreferences) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs new file mode 100644 index 0000000000..f44457160f --- /dev/null +++ b/src/Simplex/Chat/Types/Shared.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Types.Shared where + +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Char8 as B +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +import Simplex.Chat.Types.Util +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Util ((<$?>)) + +data GroupMemberRole + = GRObserver -- connects to all group members and receives all messages, can't send messages + | GRAuthor -- reserved, unused + | GRMember -- + can send messages to all group members + | GRAdmin -- + add/remove members, change member role (excl. Owners) + | GROwner -- + delete and change group information, add/remove/change roles for Owners + deriving (Eq, Show, Ord) + +instance FromField GroupMemberRole where fromField = fromBlobField_ strDecode + +instance ToField GroupMemberRole where toField = toField . strEncode + +instance StrEncoding GroupMemberRole where + strEncode = \case + GROwner -> "owner" + GRAdmin -> "admin" + GRMember -> "member" + GRAuthor -> "author" + GRObserver -> "observer" + strDecode = \case + "owner" -> Right GROwner + "admin" -> Right GRAdmin + "member" -> Right GRMember + "author" -> Right GRAuthor + "observer" -> Right GRObserver + r -> Left $ "bad GroupMemberRole " <> B.unpack r + strP = strDecode <$?> A.takeByteString + +instance FromJSON GroupMemberRole where + parseJSON = strParseJSON "GroupMemberRole" + +instance ToJSON GroupMemberRole where + toJSON = strToJSON + toEncoding = strToJEncoding diff --git a/src/Simplex/Chat/Types/UITheme.hs b/src/Simplex/Chat/Types/UITheme.hs new file mode 100644 index 0000000000..9f9c106d1f --- /dev/null +++ b/src/Simplex/Chat/Types/UITheme.hs @@ -0,0 +1,166 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.Types.UITheme where + +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import qualified Data.Attoparsec.ByteString.Char8 as A +import Data.Char (toLower) +import Data.Maybe (fromMaybe) +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +import Simplex.Chat.Types.Util +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_) +import Simplex.Messaging.Util ((<$?>)) + +data UIThemes = UIThemes + { light :: Maybe UITheme, + dark :: Maybe UITheme, + simplex :: Maybe UITheme + } + deriving (Eq, Show) + +data UITheme = UITheme + { base :: ThemeColorScheme, + wallpaper :: Maybe ChatWallpaper, + colors :: UIColors + } + deriving (Eq, Show) + +data UIColorMode = UCMLight | UCMDark + deriving (Eq, Show) + +data UIThemeEntityOverrides = UIThemeEntityOverrides + { light :: Maybe UIThemeEntityOverride, + dark :: Maybe UIThemeEntityOverride + } + deriving (Eq, Show) + +data UIThemeEntityOverride = UIThemeEntityOverride + { mode :: UIColorMode, + wallpaper :: Maybe ChatWallpaper, + colors :: UIColors + } + deriving (Eq, Show) + +data ThemeColorScheme = TCSLight | TCSDark | TCSSimplex + deriving (Eq, Show) + +data UIColorScheme + = UCSSystem + | UCSLight + | UCSDark + | UCSSimplex + deriving (Show) + +data DarkColorScheme = DCSDark | DCSSimplex + deriving (Show) + +instance StrEncoding ThemeColorScheme where + strEncode = \case + TCSLight -> "LIGHT" + TCSDark -> "DARK" + TCSSimplex -> "SIMPLEX" + strDecode = \case + "LIGHT" -> Right TCSLight + "DARK" -> Right TCSDark + "SIMPLEX" -> Right TCSSimplex + _ -> Left "bad ColorScheme" + strP = strDecode <$?> A.takeTill (== ' ') + +instance FromJSON ThemeColorScheme where + parseJSON = strParseJSON "ThemeColorScheme" + +instance ToJSON ThemeColorScheme where + toJSON = strToJSON + toEncoding = strToJEncoding + +data ChatWallpaper = ChatWallpaper + { preset :: Maybe ChatWallpaperPreset, + imageFile :: Maybe FilePath, + background :: Maybe UIColor, + tint :: Maybe UIColor, + scaleType :: Maybe ChatWallpaperScale, + scale :: Maybe Double + } + deriving (Eq, Show) + +data ChatWallpaperScale = CWSFill | CWSFit | CWSRepeat + deriving (Eq, Show) + +data UIColors = UIColors + { accent :: Maybe UIColor, + accentVariant :: Maybe UIColor, + secondary :: Maybe UIColor, + secondaryVariant :: Maybe UIColor, + background :: Maybe UIColor, + menus :: Maybe UIColor, + title :: Maybe UIColor, + sentMessage :: Maybe UIColor, + receivedMessage :: Maybe UIColor + } + deriving (Eq, Show) + +defaultUIColors :: UIColors +defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + +data ChatWallpaperPreset + = CWPKids + | CWPCats + | CWPPets + | CWPFlowers + | CWPHearts + | CWPSocial + | CWPTravel + | CWPInternet + | CWPSpace + | CWPSchool + deriving (Eq, Show) + +newtype UIColor = UIColor String + deriving (Eq, Show) + +instance FromJSON UIColor where + parseJSON v = toColor =<< J.parseJSON v + where + toColor s@('#' : cs) + | length cs == 8 && all hexDigit cs = pure $ UIColor s + toColor _ = fail "bad UIColor" + hexDigit c = (c >= '0' && c <= '9') || (let c' = toLower c in c' >= 'a' && c' <= 'f') + +instance ToJSON UIColor where + toJSON (UIColor t) = J.toJSON t + toEncoding (UIColor t) = J.toEncoding t + +$(JQ.deriveJSON (enumJSON $ dropPrefix "DCS") ''DarkColorScheme) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "UCM") ''UIColorMode) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "UCS") ''UIColorScheme) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "CWS") ''ChatWallpaperScale) + +$(JQ.deriveJSON (enumJSON $ dropPrefix "CWP") ''ChatWallpaperPreset) + +$(JQ.deriveJSON defaultJSON ''ChatWallpaper) + +$(JQ.deriveJSON defaultJSON ''UIColors) + +$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverride) + +$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverrides) + +$(JQ.deriveJSON defaultJSON ''UITheme) + +$(JQ.deriveJSON defaultJSON ''UIThemes) + +instance ToField UIThemeEntityOverrides where + toField = toField . encodeJSON + +instance FromField UIThemeEntityOverrides where + fromField = fromTextField_ $ Just . fromMaybe (UIThemeEntityOverrides Nothing Nothing) . decodeJSON diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 65a6626308..96b742ffa4 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -49,6 +49,8 @@ import Simplex.Chat.Store (AutoAccept (..), StoreError (..), UserContactLink (.. import Simplex.Chat.Styled import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared +import Simplex.Chat.Types.UITheme import qualified Simplex.FileTransfer.Transport as XFTPTransport import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) @@ -82,7 +84,7 @@ serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . response responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case - CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile + CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes CRUsersList users -> viewUsersList users CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] @@ -341,7 +343,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} -> ["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName] CRRemoteCtrlStopped {} -> ["remote controller stopped"] - CRContactPQAllowed u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": enable " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption"] CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"] CRSQLResult rows -> map plain rows CRSlowSQLQueries {chatQueries, agentQueries} -> @@ -351,8 +352,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe <> (" :: avg: " <> sShow timeAvg <> " ms") <> (" :: " <> plain (T.unwords $ T.lines query)) in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) - CRDebugLocks {chatLockName, agentLocks} -> + CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, + plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks), plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) ] CRAgentStats stats -> map (plain . intercalate ",") stats @@ -534,60 +536,68 @@ viewChats ts tz = concatMap chatPreview . reverse _ -> [] viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] -viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, content, quotedItem, file} doShow ts tz = +viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwardedByMember}, content, quotedItem, file} doShow ts tz = withGroupMsgForwarded . withItemDeleted <$> viewCI where viewCI = case chat of DirectChat c -> case chatDir of CIDirectSnd -> case content of - CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc + CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc CISndGroupEvent {} -> showSndItemProhibited to _ -> showSndItem to where to = ttyToContact' c CIDirectRcv -> case content of - CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc + CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvGroupEvent {} -> showRcvItemProhibited from _ -> showRcvItem from where from = ttyFromContact c where - quote = maybe [] (directQuote chatDir) quotedItem + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (directQuote chatDir) + quotedItem GroupChat g -> case chatDir of CIGroupSnd -> case content of - CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to quote mc + CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc CISndGroupInvitation {} -> showSndItemProhibited to _ -> showSndItem to where to = ttyToGroup g CIGroupRcv m -> case content of - CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from quote mc + CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvGroupInvitation {} -> showRcvItemProhibited from - CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) quote meta [plainContent content] False - CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g m) quote meta [plainContent content] False + CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False + CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False _ -> showRcvItem from where from = ttyFromGroup g m where - quote = maybe [] (groupQuote g) quotedItem + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (groupQuote g) + quotedItem LocalChat _ -> case chatDir of CILocalSnd -> case content of - CISndMsgContent mc -> hideLive meta $ withLocalFile to $ sndMsg to quote mc + CISndMsgContent mc -> hideLive meta $ withLocalFile to $ sndMsg to context mc CISndGroupEvent {} -> showSndItemProhibited to _ -> showSndItem to where to = "* " CILocalRcv -> case content of - CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from quote mc + CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvGroupEvent {} -> showRcvItemProhibited from _ -> showRcvItem from where from = "* " where - quote = [] + context = maybe [] forwardedFrom itemForwarded ContactRequest {} -> [] ContactConnection {} -> [] withItemDeleted item = case chatItemDeletedText ci (chatInfoMembership chat) of @@ -602,10 +612,10 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, withFile view dir l = maybe l (\f -> l <> view dir f ts tz meta) file sndMsg = msg viewSentMessage rcvMsg = msg viewReceivedMessage - msg view dir quote mc = case (msgContentText mc, file, quote) of + msg view dir context mc = case (msgContentText mc, file, context) of ("", Just _, []) -> [] - ("", Just CIFile {fileName}, _) -> view dir quote (MCText $ T.pack fileName) ts tz meta - _ -> view dir quote mc ts tz meta + ("", Just CIFile {fileName}, _) -> view dir context (MCText $ T.pack fileName) ts tz meta + _ -> view dir context mc ts tz meta showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content] meta showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content] False showSndItemProhibited to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> " " <> prohibited] meta @@ -615,11 +625,12 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {forwardedByMember}, prohibited = styled (colored Red) ("[unexpected chat item created, please report to developers]" :: String) viewChatItemInfo :: AChatItem -> ChatItemInfo -> TimeZone -> [StyledString] -viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTimed, createdAt}}) ChatItemInfo {itemVersions} tz = +viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTimed, createdAt}}) ChatItemInfo {itemVersions, forwardedFromChatItem} tz = ["sent at: " <> ts itemTs] <> receivedAt <> toBeDeletedAt <> versions + <> forwardedFrom' where ts = styleTime . localTs tz receivedAt = case msgDir of @@ -632,7 +643,21 @@ viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTime if null itemVersions then [] else ["message history:"] <> concatMap version itemVersions - version ChatItemVersion {msgContent, itemVersionTs} = prependFirst (ts itemVersionTs <> styleTime ": ") $ ttyMsgContent msgContent + where + version ChatItemVersion {msgContent, itemVersionTs} = prependFirst (ts itemVersionTs <> styleTime ": ") $ ttyMsgContent msgContent + forwardedFrom' = + case forwardedFromChatItem of + Just fwdACI@(AChatItem _ fwdMsgDir fwdChatInfo _) -> + [plain $ "forwarded from: " <> maybe "" (<> ", ") fwdDir_ <> fwdItemId] + where + fwdDir_ = case (fwdMsgDir, fwdChatInfo) of + (SMDSnd, DirectChat ct) -> Just $ "you @" <> viewContactName ct + (SMDRcv, DirectChat ct) -> Just $ "@" <> viewContactName ct + (SMDSnd, GroupChat gInfo) -> Just $ "you #" <> viewGroupName gInfo + (SMDRcv, GroupChat gInfo) -> Just $ "#" <> viewGroupName gInfo + _ -> Nothing + fwdItemId = "chat item id: " <> (T.pack . show $ aChatItemId fwdACI) + _ -> [] localTs :: TimeZone -> UTCTime -> String localTs tz ts = do @@ -664,37 +689,45 @@ viewDeliveryReceipt = \case MRBadMsgHash -> ttyError' "⩗!" viewItemUpdate :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] -viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemEdited, itemLive}, content, quotedItem} liveItems ts tz = case chat of +viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, itemEdited, itemLive}, content, quotedItem} liveItems ts tz = case chat of DirectChat c -> case chatDir of CIDirectRcv -> case content of CIRcvMsgContent mc | itemLive == Just True && not liveItems -> [] - | otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta + | otherwise -> viewReceivedUpdatedMessage from context mc ts tz meta _ -> [] where from = if itemEdited then ttyFromContactEdited c else ttyFromContact c CIDirectSnd -> case content of - CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta + CISndMsgContent mc -> hideLive meta $ viewSentMessage to context mc ts tz meta _ -> [] where to = if itemEdited then ttyToContactEdited' c else ttyToContact' c where - quote = maybe [] (directQuote chatDir) quotedItem + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (directQuote chatDir) + quotedItem GroupChat g -> case chatDir of CIGroupRcv m -> case content of CIRcvMsgContent mc | itemLive == Just True && not liveItems -> [] - | otherwise -> viewReceivedUpdatedMessage from quote mc ts tz meta + | otherwise -> viewReceivedUpdatedMessage from context mc ts tz meta _ -> [] where from = if itemEdited then ttyFromGroupEdited g m else ttyFromGroup g m CIGroupSnd -> case content of - CISndMsgContent mc -> hideLive meta $ viewSentMessage to quote mc ts tz meta + CISndMsgContent mc -> hideLive meta $ viewSentMessage to context mc ts tz meta _ -> [] where to = if itemEdited then ttyToGroupEdited g else ttyToGroup g where - quote = maybe [] (groupQuote g) quotedItem + context = + maybe + (maybe [] forwardedFrom itemForwarded) + (groupQuote g) + quotedItem _ -> [] hideLive :: CIMeta c d -> [StyledString] -> [StyledString] @@ -776,6 +809,14 @@ directQuote _ CIQuote {content = qmc, chatDir = quoteDir} = groupQuote :: GroupInfo -> CIQuote 'CTGroup -> [StyledString] groupQuote g CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc . ttyQuotedMember $ sentByMember g quoteDir +forwardedFrom :: CIForwardedFrom -> [StyledString] +forwardedFrom = \case + CIFFUnknown -> ["-> forwarded"] + CIFFContact c MDSnd _ _ -> ["<- you @" <> (plain . viewName) c] + CIFFContact c MDRcv _ _ -> ["<- @" <> (plain . viewName) c] + CIFFGroup g MDSnd _ _ -> ["<- you #" <> (plain . viewName) g] + CIFFGroup g MDRcv _ _ -> ["<- #" <> (plain . viewName) g] + sentByMember :: GroupInfo -> CIQDirection 'CTGroup -> Maybe GroupMember sentByMember GroupInfo {membership} = \case CIQGroupSnd -> Just membership @@ -834,7 +875,9 @@ viewChatCleared :: AChatInfo -> [StyledString] viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of DirectChat ct -> [ttyContact' ct <> ": all messages are removed locally ONLY"] GroupChat gi -> [ttyGroup' gi <> ": all messages are removed locally ONLY"] - _ -> [] + LocalChat _ -> ["notes: all messages are removed"] + ContactRequest _ -> [] + ContactConnection _ -> [] viewContactsList :: [Contact] -> [StyledString] viewContactsList = @@ -1167,7 +1210,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = ] viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, customData} stats incognitoProfile = +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> maybe [] viewConnectionStats stats <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink @@ -1179,15 +1222,20 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta <> [viewConnectionVerified (contactSecurityCode ct)] <> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn] <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn + <> viewUITheme uiThemes <> viewCustomData customData viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString] -viewGroupInfo GroupInfo {groupId, customData} s = +viewGroupInfo GroupInfo {groupId, uiThemes, customData} s = [ "group ID: " <> sShow groupId, "current members: " <> sShow (currentMembers s) ] + <> viewUITheme uiThemes <> viewCustomData customData +viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString] +viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> plain (LB.toStrict $ J.encode uiThemes)]) + viewCustomData :: Maybe CustomData -> [StyledString] viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)]) @@ -1482,17 +1530,17 @@ viewReceivedUpdatedMessage :: StyledString -> [StyledString] -> MsgContent -> Cu viewReceivedUpdatedMessage = viewReceivedMessage_ True viewReceivedMessage_ :: Bool -> StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewReceivedMessage_ updated from quote mc ts tz meta = receivedWithTime_ ts tz from quote meta (ttyMsgContent mc) updated +viewReceivedMessage_ updated from context mc ts tz meta = receivedWithTime_ ts tz from context meta (ttyMsgContent mc) updated viewReceivedReaction :: StyledString -> [StyledString] -> StyledString -> CurrentTime -> TimeZone -> UTCTime -> [StyledString] viewReceivedReaction from styledMsg reactionText ts tz reactionTs = prependFirst (ttyMsgTime ts tz reactionTs <> " " <> from) (styledMsg <> [" " <> reactionText]) receivedWithTime_ :: CurrentTime -> TimeZone -> StyledString -> [StyledString] -> CIMeta c d -> [StyledString] -> Bool -> [StyledString] -receivedWithTime_ ts tz from quote CIMeta {itemId, itemTs, itemEdited, itemDeleted, itemLive} styledMsg updated = do - prependFirst (ttyMsgTime ts tz itemTs <> " " <> from) (quote <> prependFirst (indent <> live) styledMsg) +receivedWithTime_ ts tz from context CIMeta {itemId, itemTs, itemEdited, itemDeleted, itemLive} styledMsg updated = do + prependFirst (ttyMsgTime ts tz itemTs <> " " <> from) (context <> prependFirst (indent <> live) styledMsg) where - indent = if null quote then "" else " " + indent = if null context then "" else " " live | itemEdited || isJust itemDeleted = "" | otherwise = case itemLive of @@ -1520,9 +1568,9 @@ recent now tz time = do || (localNow < currentDay12 && localTime >= previousDay18 && localTimeDay < localNowDay) viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewSentMessage to quote mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ quote <> prependFirst (indent <> live) (ttyMsgContent mc)) meta +viewSentMessage to context mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ context <> prependFirst (indent <> live) (ttyMsgContent mc)) meta where - indent = if null quote then "" else " " + indent = if null context then "" else " " live | itemEdited || isJust itemDeleted = "" | otherwise = case itemLive of @@ -1595,7 +1643,7 @@ standaloneUploadComplete FileTransferMeta {fileId, fileName} = \case [] -> [fileTransferStr fileId fileName <> " upload complete."] uris -> fileTransferStr fileId fileName <> " upload complete. download with:" - : map plain uris + : map plain uris sndFile :: SndFileTransfer -> StyledString sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName @@ -1924,6 +1972,8 @@ viewChatError logLevel testView = \case CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"] CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."] CEInvalidQuote -> ["cannot reply to this message"] + CEInvalidForward -> ["cannot forward this message"] + CEForwardNoFile -> ["cannot forward this message, file not found"] CEInvalidChatItemUpdate -> ["cannot update this item"] CEInvalidChatItemDelete -> ["cannot delete this item"] CEHasCurrentCall -> ["call already in progress"] diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index b78d36f489..7cd775c5ee 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -19,7 +19,8 @@ import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Core import Simplex.Chat.Options (CoreChatOpts (..)) -import Simplex.Chat.Types (GroupMemberRole (..), Profile (..)) +import Simplex.Chat.Types (Profile (..)) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import System.FilePath (()) import Test.Hspec hiding (it) @@ -808,7 +809,9 @@ testRestoreDirectory tmp = do groupFoundN 3 bob "privacy" groupFound bob "security" groupFoundN 3 cath "privacy" - groupFound cath "security" + cath #> "@SimpleX-Directory security" + cath <## "SimpleX-Directory: quantum resistant end-to-end encryption enabled" + groupFoundN' 2 cath "security" listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () listGroups superUser bob cath = do @@ -1054,6 +1057,10 @@ groupFound = groupFoundN 2 groupFoundN :: Int -> TestCC -> String -> IO () groupFoundN count u name = do u #> ("@SimpleX-Directory " <> name) + groupFoundN' count u name + +groupFoundN' :: Int -> TestCC -> String -> IO () +groupFoundN' count u name = do u <# ("SimpleX-Directory> > " <> name) u <## " Found 1 group(s)." u <#. ("SimpleX-Directory> " <> name) diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index a827236a15..4897a3b3d3 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -44,7 +44,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), closeSQLiteStore) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig) -import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM @@ -146,8 +146,8 @@ testAgentCfgVPrev :: AgentConfig testAgentCfgVPrev = testAgentCfg { smpClientVRange = prevRange $ smpClientVRange testAgentCfg, - smpAgentVRange = \_ -> prevRange $ supportedSMPAgentVRange PQSupportOff, - e2eEncryptVRange = \_ -> prevRange $ supportedE2EEncryptVRange PQSupportOff, + smpAgentVRange = prevRange supportedSMPAgentVRange, + e2eEncryptVRange = prevRange supportedE2EEncryptVRange, smpCfg = (smpCfg testAgentCfg) {serverVRange = prevRange $ serverVRange $ smpCfg testAgentCfg} } @@ -155,8 +155,8 @@ testAgentCfgVNext :: AgentConfig testAgentCfgVNext = testAgentCfg { smpClientVRange = nextRange $ smpClientVRange testAgentCfg, - smpAgentVRange = \_ -> mkVersionRange duplexHandshakeSMPAgentVersion $ max pqdrSMPAgentVersion currentSMPAgentVersion, - e2eEncryptVRange = \_ -> mkVersionRange CR.kdfX3DHE2EEncryptVersion $ max CR.pqRatchetE2EEncryptVersion CR.currentE2EEncryptVersion, + smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion $ max pqdrSMPAgentVersion currentSMPAgentVersion, + e2eEncryptVRange = mkVersionRange CR.kdfX3DHE2EEncryptVersion $ max CR.pqRatchetE2EEncryptVersion CR.currentE2EEncryptVersion, smpCfg = (smpCfg testAgentCfg) {serverVRange = nextRange $ serverVRange $ smpCfg testAgentCfg} } @@ -164,29 +164,29 @@ testAgentCfgV1 :: AgentConfig testAgentCfgV1 = testAgentCfg { smpClientVRange = v1Range, - smpAgentVRange = \_ -> versionToRange duplexHandshakeSMPAgentVersion, - e2eEncryptVRange = \_ -> versionToRange CR.kdfX3DHE2EEncryptVersion, + smpAgentVRange = versionToRange duplexHandshakeSMPAgentVersion, + e2eEncryptVRange = versionToRange CR.kdfX3DHE2EEncryptVersion, smpCfg = (smpCfg testAgentCfg) {serverVRange = versionToRange batchCmdsSMPVersion} } testCfgVPrev :: ChatConfig testCfgVPrev = testCfg - { chatVRange = \_ -> prevRange $ chatVRange testCfg PQSupportOff, + { chatVRange = prevRange $ chatVRange testCfg, agentConfig = testAgentCfgVPrev } testCfgVNext :: ChatConfig testCfgVNext = testCfg - { chatVRange = \_ -> mkVersionRange initialChatVersion $ max pqEncryptionCompressionVersion currentChatVersion, + { chatVRange = mkVersionRange initialChatVersion $ max pqEncryptionCompressionVersion currentChatVersion, agentConfig = testAgentCfgVNext } testCfgV1 :: ChatConfig testCfgV1 = testCfg - { chatVRange = const v1Range, + { chatVRange = v1Range, agentConfig = testAgentCfgV1 } @@ -210,7 +210,7 @@ testCfgCreateGroupDirect = mkCfgCreateGroupDirect testCfg mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig -mkCfgCreateGroupDirect cfg = cfg {chatVRange = const groupCreateDirectVRange} +mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange} groupCreateDirectVRange :: VersionRangeChat groupCreateDirectVRange = mkVersionRange (VersionChat 1) (VersionChat 1) @@ -220,7 +220,7 @@ testCfgGroupLinkViaContact = mkCfgGroupLinkViaContact testCfg mkCfgGroupLinkViaContact :: ChatConfig -> ChatConfig -mkCfgGroupLinkViaContact cfg = cfg {chatVRange = const groupLinkViaContactVRange} +mkCfgGroupLinkViaContact cfg = cfg {chatVRange = groupLinkViaContactVRange} groupLinkViaContactVRange :: VersionRangeChat groupLinkViaContactVRange = mkVersionRange (VersionChat 1) (VersionChat 2) diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 77d256a240..e8f3838eb6 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -3,6 +3,7 @@ module ChatTests where import ChatTests.ChatList import ChatTests.Direct import ChatTests.Files +import ChatTests.Forward import ChatTests.Groups import ChatTests.Local import ChatTests.Profiles @@ -11,6 +12,7 @@ import Test.Hspec hiding (it) chatTests :: SpecWith FilePath chatTests = do describe "direct tests" chatDirectTests + describe "forward tests" chatForwardTests describe "group tests" chatGroupTests describe "local chats tests" chatLocalChatsTests describe "file tests" chatFileTests diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 4b0389ab90..82eb4def39 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -10,7 +10,7 @@ import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) -import Control.Monad (forM_, when) +import Control.Monad (forM_) import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B @@ -21,11 +21,10 @@ import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Options (ChatOpts (..)) -import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion, supportedChatVRange) +import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) -import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, VersionChat, pattern VersionChat) +import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import Simplex.Messaging.Util (safeDecodeUtf8) import Simplex.Messaging.Version import System.Directory (copyFile, doesDirectoryExist, doesFileExist) @@ -47,6 +46,7 @@ chatDirectTests = do it "direct timed message" testDirectTimedMessage it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact it "should send multiline message" testMultilineMessage + it "send large message" testLargeMessage describe "duplicate contacts" $ do it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate @@ -116,25 +116,18 @@ chatDirectTests = do it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts describe "negotiate connection peer chat protocol version range" $ do describe "peer version range correctly set for new connection via invitation" $ do - testInvVRange (supportedChatVRange PQSupportOff) (supportedChatVRange PQSupportOff) - testInvVRange (supportedChatVRange PQSupportOff) vr11 - testInvVRange vr11 (supportedChatVRange PQSupportOff) + testInvVRange supportedChatVRange supportedChatVRange + testInvVRange supportedChatVRange vr11 + testInvVRange vr11 supportedChatVRange testInvVRange vr11 vr11 describe "peer version range correctly set for new connection via contact request" $ do - testReqVRange (supportedChatVRange PQSupportOff) (supportedChatVRange PQSupportOff) - testReqVRange (supportedChatVRange PQSupportOff) vr11 - testReqVRange vr11 (supportedChatVRange PQSupportOff) + testReqVRange supportedChatVRange supportedChatVRange + testReqVRange supportedChatVRange vr11 + testReqVRange vr11 supportedChatVRange testReqVRange vr11 vr11 it "update peer version range on received messages" testUpdatePeerChatVRange describe "network statuses" $ do it "should get network statuses" testGetNetworkStatuses - describe "PQ tests" $ do - describe "enable PQ before connection, connect via invitation link" $ pqMatrix2 runTestPQConnectViaLink - describe "enable PQ before connection, connect via contact address" $ pqMatrix2 runTestPQConnectViaAddress - describe "connect via invitation link with PQ encryption enabled" $ pqVersionTestMatrix2 runTestPQVersionsViaLink - describe "connect via contact address with PQ encryption enabled" $ pqVersionTestMatrix2 runTestPQVersionsViaAddress - it "should enable PQ after several messages in connection without PQ" testPQEnableContact - it "should enable PQ, reduce envelope size and enable compression" testPQEnableContactCompression where testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2 testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2 @@ -142,7 +135,7 @@ chatDirectTests = do testAddContact :: HasCallStack => SpecWith FilePath testAddContact = versionTestMatrix2 runTestAddContact where - runTestAddContact alice bob = do + runTestAddContact pqExpected alice bob = do alice ##> "/_connect 1" inv <- getInvitation alice bob ##> ("/_connect 1 " <> inv) @@ -151,46 +144,50 @@ testAddContact = versionTestMatrix2 runTestAddContact (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 - chatsEmpty alice bob + chatsEmpty alice #> "@bob hello there 🙂" bob <# "alice> hello there 🙂" alice ##> "/_unread chat @2 on" alice <## "ok" alice ##> "/_unread chat @2 off" alice <## "ok" - chatsOneMessage alice bob + chatsOneMessage bob #> "@alice hello there" alice <# "bob> hello there" bob #> "@alice how are you?" alice <# "bob> how are you?" - chatsManyMessages alice bob - chatsEmpty alice bob = do - alice @@@ [("@bob", lastChatFeature)] - alice #$> ("/_get chat @2 count=100", chat, chatFeatures) - bob @@@ [("@alice", lastChatFeature)] - bob #$> ("/_get chat @2 count=100", chat, chatFeatures) - chatsOneMessage alice bob = do - alice @@@ [("@bob", "hello there 🙂")] - alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) - bob @@@ [("@alice", "hello there 🙂")] - bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂")]) - chatsManyMessages alice bob = do - alice @@@ [("@bob", "how are you?")] - alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) - bob @@@ [("@alice", "how are you?")] - bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) - -- pagination - alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")]) - alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, chatFeatures <> [(1, "hello there 🙂")]) - -- search - alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) - -- read messages - alice #$> ("/_read chat @2 from=1 to=100", id, "ok") - bob #$> ("/_read chat @2 from=1 to=100", id, "ok") - alice #$> ("/_read chat @2", id, "ok") - bob #$> ("/_read chat @2", id, "ok") - alice #$> ("/read user", id, "ok") - alice #$> ("/_read user 1", id, "ok") + chatsManyMessages + where + chatsEmpty = do + alice @@@ [("@bob", lastChatFeature)] + alice #$> ("/_get chat @2 count=100", chat, features) + bob @@@ [("@alice", lastChatFeature)] + bob #$> ("/_get chat @2 count=100", chat, features) + chatsOneMessage = do + alice @@@ [("@bob", "hello there 🙂")] + alice #$> ("/_get chat @2 count=100", chat, features <> [(1, "hello there 🙂")]) + bob @@@ [("@alice", "hello there 🙂")] + bob #$> ("/_get chat @2 count=100", chat, features <> [(0, "hello there 🙂")]) + chatsManyMessages = do + alice @@@ [("@bob", "how are you?")] + alice #$> ("/_get chat @2 count=100", chat, features <> [(1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) + bob @@@ [("@alice", "how are you?")] + bob #$> ("/_get chat @2 count=100", chat, features <> [(0, "hello there 🙂"), (1, "hello there"), (1, "how are you?")]) + -- pagination + alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")]) + alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, features <> [(1, "hello there 🙂")]) + -- search + alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) + -- read messages + alice #$> ("/_read chat @2 from=1 to=100", id, "ok") + bob #$> ("/_read chat @2 from=1 to=100", id, "ok") + alice #$> ("/_read chat @2", id, "ok") + bob #$> ("/_read chat @2", id, "ok") + alice #$> ("/read user", id, "ok") + alice #$> ("/_read user 1", id, "ok") + features = if pqExpected + then chatFeatures + else (0, e2eeInfoNoPQStr) : tail chatFeatures testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO () testDuplicateContactsSeparate = @@ -728,6 +725,20 @@ testMultilineMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bo cath <# "alice> hello" cath <## "there" +testLargeMessage :: HasCallStack => FilePath -> IO () +testLargeMessage = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + img <- genProfileImg + let profileImage = "data:image/png;base64," <> B.unpack img + alice `send` ("/_profile 1 {\"displayName\": \"alice2\", \"fullName\": \"\", \"image\": \"" <> profileImage <> "\"}") + _trimmedCmd1 <- getTermLine alice + alice <## "user profile is changed to alice2 (your 1 contacts are notified)" + bob <## "contact alice changed to alice2" + bob <## "use @alice2 to send messages" + testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ @@ -1649,17 +1660,17 @@ testUsersDifferentCIExpirationTTL tmp = do -- set ttl for first user alice ##> "/user alice" showActiveUser alice "alice (Alice)" - alice #$> ("/_ttl 1 1", id, "ok") + alice #$> ("/_ttl 1 2", id, "ok") -- set ttl for second user alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_ttl 2 3", id, "ok") + alice #$> ("/_ttl 2 4", id, "ok") -- first user messages alice ##> "/user alice" showActiveUser alice "alice (Alice)" - alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)") + alice #$> ("/ttl", id, "old messages are set to be deleted after: 2 second(s)") alice #> "@bob alice 3" bob <# "alice> alice 3" @@ -1671,7 +1682,7 @@ testUsersDifferentCIExpirationTTL tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/ttl", id, "old messages are set to be deleted after: 3 second(s)") + alice #$> ("/ttl", id, "old messages are set to be deleted after: 4 second(s)") alice #> "@bob alisa 3" bob <# "alisa> alisa 3" @@ -1680,7 +1691,7 @@ testUsersDifferentCIExpirationTTL tmp = do alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2000000 + threadDelay 3000000 -- messages both before and after setting chat item ttl are deleted -- first user messages @@ -1916,13 +1927,13 @@ testUsersTimedMessages tmp = do withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChat tmp "alice" aliceProfile $ \alice -> do connectUsers alice bob - configureTimedMessages alice bob "2" "1" + configureTimedMessages alice bob "2" "2" -- create second user and configure timed messages for contact alice ##> "/create user alisa" showActiveUser alice "alisa" connectUsers alice bob - configureTimedMessages alice bob "4" "2" + configureTimedMessages alice bob "4" "3" -- first user messages alice ##> "/user alice" @@ -1943,7 +1954,7 @@ testUsersTimedMessages tmp = do alice <# "bob> alisa 2" -- messages are deleted after ttl - threadDelay 500000 + threadDelay 1500000 alice ##> "/user alice" showActiveUser alice "alice (Alice)" @@ -2094,7 +2105,7 @@ testUserPrivacy = alice <##? chatHistory alice ##> "/_get items before=13 count=10" alice - <##? [ ConsoleString ("bob> " <> e2eeInfoNoPQStr), + <##? [ ConsoleString ("bob> " <> e2eeInfoPQStr), "bob> Disappearing messages: allowed", "bob> Full deletion: off", "bob> Message reactions: enabled", @@ -2165,7 +2176,7 @@ testUserPrivacy = alice <## "messages are shown" alice <## "profile is visible" chatHistory = - [ ConsoleString ("bob> " <> e2eeInfoNoPQStr), + [ ConsoleString ("bob> " <> e2eeInfoPQStr), "bob> Disappearing messages: allowed", "bob> Full deletion: off", "bob> Message reactions: enabled", @@ -2347,6 +2358,7 @@ testMarkContactVerified = alice <## "sending messages via: localhost" alice <## "you've shared main profile with this contact" alice <## connVerified + alice <## "quantum resistant end-to-end encryption" alice <## currentChatVRangeInfo where connVerified @@ -2517,6 +2529,7 @@ testSyncRatchetCodeReset tmp = bob <## "sending messages via: localhost" bob <## "you've shared main profile with this contact" bob <## connVerified + bob <## "quantum resistant end-to-end encryption" bob <## currentChatVRangeInfo where connVerified @@ -2667,8 +2680,8 @@ testConfigureDeliveryReceipts tmp = testConnInvChatVRange :: HasCallStack => VersionRangeChat -> VersionRangeChat -> FilePath -> IO () testConnInvChatVRange ct1VRange ct2VRange tmp = - withNewTestChatCfg tmp testCfg {chatVRange = const ct1VRange} "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp testCfg {chatVRange = const ct2VRange} "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do connectUsers alice bob alice ##> "/i bob" @@ -2679,8 +2692,8 @@ testConnInvChatVRange ct1VRange ct2VRange tmp = testConnReqChatVRange :: HasCallStack => VersionRangeChat -> VersionRangeChat -> FilePath -> IO () testConnReqChatVRange ct1VRange ct2VRange tmp = - withNewTestChatCfg tmp testCfg {chatVRange = const ct1VRange} "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp testCfg {chatVRange = const ct2VRange} "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do alice ##> "/ad" cLink <- getContactLink alice True bob ##> ("/c " <> cLink) @@ -2707,7 +2720,7 @@ testUpdatePeerChatVRange tmp = contactInfoChatVRange alice vr11 bob ##> "/i alice" - contactInfoChatVRange bob (supportedChatVRange PQSupportOff) + contactInfoChatVRange bob supportedChatVRange withTestChat tmp "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" @@ -2716,10 +2729,10 @@ testUpdatePeerChatVRange tmp = alice <# "bob> hello 1" alice ##> "/i bob" - contactInfoChatVRange alice (supportedChatVRange PQSupportOff) + contactInfoChatVRange alice supportedChatVRange bob ##> "/i alice" - contactInfoChatVRange bob (supportedChatVRange PQSupportOff) + contactInfoChatVRange bob supportedChatVRange withTestChatCfg tmp cfg11 "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" @@ -2731,9 +2744,9 @@ testUpdatePeerChatVRange tmp = contactInfoChatVRange alice vr11 bob ##> "/i alice" - contactInfoChatVRange bob (supportedChatVRange PQSupportOff) + contactInfoChatVRange bob supportedChatVRange where - cfg11 = testCfg {chatVRange = const vr11} :: ChatConfig + cfg11 = testCfg {chatVRange = vr11} :: ChatConfig testGetNetworkStatuses :: HasCallStack => FilePath -> IO () testGetNetworkStatuses tmp = do @@ -2759,259 +2772,5 @@ contactInfoChatVRange cc (VersionRange minVer maxVer) = do cc <## "sending messages via: localhost" cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## "quantum resistant end-to-end encryption" cc <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")") - -runTestPQConnectViaLink :: HasCallStack => (TestCC, PQEnabled) -> (TestCC, PQEnabled) -> IO () -runTestPQConnectViaLink (alice, aPQ) (bob, bPQ) = do - when aPQ $ pqOn alice - when bPQ $ pqOn bob - - connectUsers alice bob - - (alice, "hi") `pqSend` bob - (bob, "hey") `pqSend` alice - - alice ##> "/_get chat @2 count=100" - ra <- chat <$> getTermLine alice - ra `shouldContain` [(0, e2eeInfo)] - alice `pqForContact` 2 `shouldReturn` PQEncryption pqEnabled - - bob ##> "/_get chat @2 count=100" - rb <- chat <$> getTermLine bob - rb `shouldContain` [(0, e2eeInfo)] - bob `pqForContact` 2 `shouldReturn` PQEncryption pqEnabled - where - pqEnabled = aPQ && bPQ - pqSend = if pqEnabled then (+#>) else (\#>) - e2eeInfo = if pqEnabled then e2eeInfoPQStr else e2eeInfoNoPQStr - -pqOn :: TestCC -> IO () -pqOn cc = do - cc ##> "/pq on" - cc <## "ok" - -runTestPQConnectViaAddress :: HasCallStack => (TestCC, PQEnabled) -> (TestCC, PQEnabled) -> IO () -runTestPQConnectViaAddress (alice, aPQ) (bob, bPQ) = do - when aPQ $ pqOn alice - when bPQ $ pqOn bob - - alice ##> "/ad" - cLink <- getContactLink alice True - bob ##> ("/c " <> cLink) - alice <#? bob - alice @@@ [("<@bob", "")] - alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." - concurrently_ - (bob <## "alice (Alice): contact is connected") - (alice <## "bob (Bob): contact is connected") - - (alice, "hi") `pqSend` bob - (bob, "hey") `pqSend` alice - - alice ##> "/_get chat @2 count=100" - ra <- chat <$> getTermLine alice - ra `shouldContain` [(0, e2eeInfo)] - alice `pqForContact` 2 `shouldReturn` PQEncryption pqEnabled - - bob ##> "/_get chat @2 count=100" - rb <- chat <$> getTermLine bob - rb `shouldContain` [(0, e2eeInfo)] - bob `pqForContact` 2 `shouldReturn` PQEncryption pqEnabled - where - pqEnabled = aPQ && bPQ - pqSend = if pqEnabled then (+#>) else (\#>) - e2eeInfo = if pqEnabled then e2eeInfoPQStr else e2eeInfoNoPQStr - -runTestPQVersionsViaLink :: HasCallStack => TestCC -> TestCC -> Bool -> VersionChat -> IO () -runTestPQVersionsViaLink alice bob pqExpected vExpected = do - img <- genProfileImg - let profileImage = "data:image/png;base64," <> B.unpack img - alice `send` ("/set profile image " <> profileImage) - _trimmedCmd1 <- getTermLine alice - alice <## "profile image updated" - bob `send` ("/set profile image " <> profileImage) - _trimmedCmd2 <- getTermLine bob - bob <## "profile image updated" - - pqOn alice - pqOn bob - - connectUsers alice bob - - (alice, "hi", vExpected) `pqSend` (bob, vExpected) - (bob, "hey", vExpected) `pqSend` (alice, vExpected) - - alice ##> "/_get chat @2 count=100" - ra <- chat <$> getTermLine alice - ra `shouldContain` [(0, e2eeInfo)] - alice `pqForContact` 2 `shouldReturn` PQEncryption pqExpected - - bob ##> "/_get chat @2 count=100" - rb <- chat <$> getTermLine bob - rb `shouldContain` [(0, e2eeInfo)] - bob `pqForContact` 2 `shouldReturn` PQEncryption pqExpected - where - pqSend = if pqExpected then (+:#>) else (\:#>) - e2eeInfo = if pqExpected then e2eeInfoPQStr else e2eeInfoNoPQStr - -runTestPQVersionsViaAddress :: HasCallStack => TestCC -> TestCC -> Bool -> VersionChat -> IO () -runTestPQVersionsViaAddress alice bob pqExpected vExpected = do - img <- genProfileImg - let profileImage = "data:image/png;base64," <> B.unpack img - alice `send` ("/set profile image " <> profileImage) - _trimmedCmd1 <- getTermLine alice - alice <## "profile image updated" - bob `send` ("/set profile image " <> profileImage) - _trimmedCmd2 <- getTermLine bob - bob <## "profile image updated" - - pqOn alice - pqOn bob - - alice ##> "/ad" - cLink <- getContactLink alice True - bob ##> ("/c " <> cLink) - alice <#? bob - alice @@@ [("<@bob", "")] - alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." - concurrently_ - (bob <## "alice (Alice): contact is connected") - (alice <## "bob (Bob): contact is connected") - - (alice, "hi", vExpected) `pqSend` (bob, vExpected) - (bob, "hey", vExpected) `pqSend` (alice, vExpected) - - alice ##> "/_get chat @2 count=100" - ra <- chat <$> getTermLine alice - ra `shouldContain` [(0, e2eeInfo)] - alice `pqForContact` 2 `shouldReturn` PQEncryption pqExpected - - bob ##> "/_get chat @2 count=100" - rb <- chat <$> getTermLine bob - rb `shouldContain` [(0, e2eeInfo)] - bob `pqForContact` 2 `shouldReturn` PQEncryption pqExpected - where - pqSend = if pqExpected then (+:#>) else (\:#>) - e2eeInfo = if pqExpected then e2eeInfoPQStr else e2eeInfoNoPQStr - -testPQEnableContact :: HasCallStack => FilePath -> IO () -testPQEnableContact = - testChat2 aliceProfile bobProfile $ \alice bob -> do - connectUsers alice bob - (alice, "hi") \#> bob - (bob, "hey") \#> alice - - alice ##> "/_get chat @2 count=100" - ra <- chat <$> getTermLine alice - ra `shouldContain` [(0, e2eeInfoNoPQStr)] - PQEncOff <- alice `pqForContact` 2 - - bob ##> "/_get chat @2 count=100" - rb <- chat <$> getTermLine bob - rb `shouldContain` [(0, e2eeInfoNoPQStr)] - PQEncOff <- bob `pqForContact` 2 - - sendMany PQEncOff alice bob - PQEncOff <- alice `pqForContact` 2 - PQEncOff <- bob `pqForContact` 2 - - -- enabling experimental flags doesn't enable PQ in previously created connection - pqOn alice - sendMany PQEncOff alice bob - PQEncOff <- alice `pqForContact` 2 - PQEncOff <- bob `pqForContact` 2 - - pqOn bob - sendMany PQEncOff alice bob - PQEncOff <- alice `pqForContact` 2 - PQEncOff <- bob `pqForContact` 2 - - -- if only one contact allows PQ, it's not enabled - alice ##> "/pq @bob on" - alice <## "bob: enable quantum resistant end-to-end encryption" - sendMany PQEncOff alice bob - PQEncOff <- alice `pqForContact` 2 - PQEncOff <- bob `pqForContact` 2 - - -- both contacts have to allow PQ to enable it - bob ##> "/pq @alice on" - bob <## "alice: enable quantum resistant end-to-end encryption" - - (alice, "1") \#> bob - (bob, "2") \#> alice - (alice, "3") \#> bob - (bob, "4") \#> alice - (alice, "5") +#> bob - - PQEncOff <- alice `pqForContact` 2 - PQEncOff <- bob `pqForContact` 2 - - (bob, "6") ++#> alice - -- equivalent to: - -- bob `send` "@alice 6" - -- bob <## "alice: quantum resistant end-to-end encryption enabled" - -- bob <# "@alice 6" - -- alice <## "bob: quantum resistant end-to-end encryption enabled" - -- alice <# "bob> 6" - - PQEncOn <- alice `pqForContact` 2 - alice #$> ("/_get chat @2 count=2", chat, [(0, e2eeInfoPQStr), (0, "6")]) - - PQEncOn <- bob `pqForContact` 2 - bob #$> ("/_get chat @2 count=2", chat, [(1, e2eeInfoPQStr), (1, "6")]) - - (alice, "6") +#> bob - (bob, "7") +#> alice - - sendMany PQEncOn alice bob - - PQEncOn <- alice `pqForContact` 2 - PQEncOn <- bob `pqForContact` 2 - pure () - -sendMany :: PQEncryption -> TestCC -> TestCC -> IO () -sendMany pqEnc alice bob = - forM_ [(1 :: Int) .. 10] $ \i -> do - sndRcv pqEnc False (alice, show i) bob - sndRcv pqEnc False (bob, show i) alice - -testPQEnableContactCompression :: HasCallStack => FilePath -> IO () -testPQEnableContactCompression = - testChat2 aliceProfile bobProfile $ \alice bob -> do - connectUsers alice bob - (alice, "hi") \#> bob - (bob, "hey") \#> alice - PQEncOff <- alice `pqForContact` 2 - PQEncOff <- bob `pqForContact` 2 - (alice, "lrg 1", v) \:#> (bob, v) - (bob, "lrg 2", v) \:#> (alice, v) - PQSupportOff <- alice `pqSupportForCt` 2 - alice ##> "/pq @bob on" - alice <## "bob: enable quantum resistant end-to-end encryption" - PQSupportOn <- alice `pqSupportForCt` 2 - (alice, "lrg 3", v) \:#> (bob, v) - (bob, "lrg 4", v) \:#> (alice, v) - PQSupportOff <- bob `pqSupportForCt` 2 - bob ##> "/pq @alice on" - bob <## "alice: enable quantum resistant end-to-end encryption" - PQSupportOn <- bob `pqSupportForCt` 2 - threadDelay 300000 - (alice, "lrg 1", v) \:#> (bob, v') - threadDelay 300000 - (bob, "lrg 2", v') \:#> (alice, v') - threadDelay 300000 - (alice, "lrg 3", v') \:#> (bob, v') - threadDelay 300000 - (bob, "lrg 4", v') \:#> (alice, v') - threadDelay 300000 - (alice, "lrg 5", v') +:#> (bob, v') - PQEncOff <- alice `pqForContact` 2 - PQEncOff <- bob `pqForContact` 2 - (bob, "lrg 6", v') ++:#> (alice, v') - (alice, "lrg 7", v') +:#> (bob, v') - (bob, "lrg 8", v') +:#> (alice, v') - where - v = currentChatVersion - v' = pqEncryptionCompressionVersion diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 1e72df9156..572d9294a9 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -617,20 +617,8 @@ testXFTPWithRelativePaths = withXFTPServer $ do -- agent is passed xftp work directory only on chat start, -- so for test we work around by stopping and starting chat - alice ##> "/_stop" - alice <## "chat stopped" - alice #$> ("/_files_folder ./tests/fixtures", id, "ok") - alice #$> ("/_temp_folder ./tests/tmp/alice_xftp", id, "ok") - alice ##> "/_start" - alice <## "chat started" - - bob ##> "/_stop" - bob <## "chat stopped" - bob #$> ("/_files_folder ./tests/tmp/bob_files", id, "ok") - bob #$> ("/_temp_folder ./tests/tmp/bob_xftp", id, "ok") - bob ##> "/_start" - bob <## "chat started" - + setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp" + setRelativePaths bob "./tests/tmp/bob_files" "./tests/tmp/bob_xftp" connectUsers alice bob alice #> "/f @bob test.pdf" @@ -787,7 +775,8 @@ testXFTPCancelRcvRepeat = bob ##> "/fr 1 ./tests/tmp" bob <### [ "saving file 1 from alice to ./tests/tmp/testfile_1", - "started receiving file 1 (testfile) from alice" + "started receiving file 1 (testfile) from alice", + StartsWith "chat db error: SERcvFileNotFoundXFTP" ] bob <## "completed receiving file 1 (testfile) from alice" diff --git a/tests/ChatTests/Forward.hs b/tests/ChatTests/Forward.hs new file mode 100644 index 0000000000..d49c6df955 --- /dev/null +++ b/tests/ChatTests/Forward.hs @@ -0,0 +1,592 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PostfixOperators #-} + +module ChatTests.Forward where + +import ChatClient +import ChatTests.Utils +import Control.Concurrent (threadDelay) +import qualified Data.ByteString.Char8 as B +import System.Directory (copyFile, doesFileExist) +import Test.Hspec hiding (it) + +chatForwardTests :: SpecWith FilePath +chatForwardTests = do + describe "forward messages" $ do + it "from contact to contact" testForwardContactToContact + it "from contact to group" testForwardContactToGroup + it "from contact to notes" testForwardContactToNotes + it "from group to contact" testForwardGroupToContact + it "from group to group" testForwardGroupToGroup + it "from group to notes" testForwardGroupToNotes + it "from notes to contact" testForwardNotesToContact + it "from notes to group" testForwardNotesToGroup + it "from notes to notes" testForwardNotesToNotes -- TODO forward between different folders when supported + describe "interactions with forwarded messages" $ do + it "preserve original forward info" testForwardPreserveInfo + it "received forwarded message is saved with new forward info" testForwardRcvMsgNewInfo + it "quoted message is not included" testForwardQuotedMsg + it "editing is prohibited" testForwardEditProhibited + it "delete for other" testForwardDeleteForOther + describe "forward files" $ do + it "from contact to contact" testForwardFileNoFilesFolder + it "with relative paths: from contact to contact" testForwardFileContactToContact + it "with relative paths: from group to notes" testForwardFileGroupToNotes + it "with relative paths: from notes to group" testForwardFileNotesToGroup + +testForwardContactToContact :: HasCallStack => FilePath -> IO () +testForwardContactToContact = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + connectUsers bob cath + + alice #> "@bob hi" + bob <# "alice> hi" + msgId <- lastItemId alice + bob #> "@alice hey" + alice <# "bob> hey" + + alice ##> ("/_forward @3 @2 " <> msgId) + alice <# "@cath <- you @bob" + alice <## " hi" + cath <# "alice> -> forwarded" + cath <## " hi" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail @cath 2" + alice <# "@cath <- you @bob" + alice <## " hi" + alice <# "@cath <- @bob" + alice <## " hey" + + cath ##> "/tail @alice 2" + cath <# "alice> -> forwarded" + cath <## " hi" + cath <# "alice> -> forwarded" + cath <## " hey" + + -- item info + alice ##> "/item info @cath hey" + alice <##. "sent at: " + alice <## "message history:" + alice .<## ": hey" + alice <##. "forwarded from: @bob, chat item id:" + +testForwardContactToGroup :: HasCallStack => FilePath -> IO () +testForwardContactToGroup = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + createGroup2 "team" alice cath + + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "#team <- @bob hi" + alice <# "#team <- you @bob" + alice <## " hi" + cath <# "#team alice> -> forwarded" + cath <## " hi" + + alice `send` "#team <- @bob hey" + alice <# "#team <- @bob" + alice <## " hey" + cath <# "#team alice> -> forwarded" + cath <## " hey" + +testForwardContactToNotes :: HasCallStack => FilePath -> IO () +testForwardContactToNotes = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createCCNoteFolder alice + connectUsers alice bob + + alice #> "@bob hi" + bob <# "alice> hi" + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "* <- @bob hi" + alice <# "* <- you @bob" + alice <## " hi" + + alice `send` "* <- @bob hey" + alice <# "* <- @bob" + alice <## " hey" + +testForwardGroupToContact :: HasCallStack => FilePath -> IO () +testForwardGroupToContact = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + connectUsers alice cath + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + alice `send` "@cath <- #team hi" + alice <# "@cath <- you #team" + alice <## " hi" + cath <# "alice> -> forwarded" + cath <## " hi" + + alice `send` "@cath <- #team @bob hey" + alice <# "@cath <- #team" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardGroupToGroup :: HasCallStack => FilePath -> IO () +testForwardGroupToGroup = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + createGroup2 "club" alice cath + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + threadDelay 1000000 + + alice `send` "#club <- #team hi" + alice <# "#club <- you #team" + alice <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hi" + + threadDelay 1000000 + + alice `send` "#club <- #team hey" + alice <# "#club <- #team" + alice <## " hey" + cath <# "#club alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail #club 2" + alice <# "#club <- you #team" + alice <## " hi" + alice <# "#club <- #team" + alice <## " hey" + + cath ##> "/tail #club 2" + cath <# "#club alice> -> forwarded" + cath <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hey" + +testForwardGroupToNotes :: HasCallStack => FilePath -> IO () +testForwardGroupToNotes = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createCCNoteFolder alice + createGroup2 "team" alice bob + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + alice `send` "* <- #team hi" + alice <# "* <- you #team" + alice <## " hi" + + alice `send` "* <- #team hey" + alice <# "* <- #team" + alice <## " hey" + +testForwardNotesToContact :: HasCallStack => FilePath -> IO () +testForwardNotesToContact = + testChat2 aliceProfile cathProfile $ + \alice cath -> do + createCCNoteFolder alice + connectUsers alice cath + + alice /* "hi" + + alice `send` "@cath <- * hi" + alice <# "@cath hi" + cath <# "alice> hi" + +testForwardNotesToGroup :: HasCallStack => FilePath -> IO () +testForwardNotesToGroup = + testChat2 aliceProfile cathProfile $ + \alice cath -> do + createCCNoteFolder alice + createGroup2 "team" alice cath + + alice /* "hi" + + alice `send` "#team <- * hi" + alice <# "#team hi" + cath <# "#team alice> hi" + +testForwardNotesToNotes :: HasCallStack => FilePath -> IO () +testForwardNotesToNotes tmp = + withNewTestChat tmp "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + alice /* "hi" + + alice `send` "* <- * hi" + alice <# "* hi" + + alice ##> "/tail * 2" + alice <# "* hi" + alice <# "* hi" + +testForwardPreserveInfo :: HasCallStack => FilePath -> IO () +testForwardPreserveInfo = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createCCNoteFolder alice + connectUsers alice bob + connectUsers alice cath + createGroup2 "team" alice dan + + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "* <- @bob hey" + alice <# "* <- @bob" + alice <## " hey" + + alice `send` "@cath <- * hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + alice `send` "#team <- @cath hey" + alice <# "#team <- @bob" + alice <## " hey" + dan <# "#team alice> -> forwarded" + dan <## " hey" + +testForwardRcvMsgNewInfo :: HasCallStack => FilePath -> IO () +testForwardRcvMsgNewInfo = + testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + connectUsers bob dan + createCCNoteFolder alice + connectUsers alice bob + connectUsers alice cath + + dan #> "@bob hey" + bob <# "dan> hey" + + bob `send` "@alice <- @dan hey" + bob <# "@alice <- @dan" + bob <## " hey" + alice <# "bob> -> forwarded" + alice <## " hey" + + alice `send` "* <- @bob hey" + alice <# "* <- @bob" + alice <## " hey" + + alice `send` "@cath <- * hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardQuotedMsg :: HasCallStack => FilePath -> IO () +testForwardQuotedMsg = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + + alice #> "@bob hi" + bob <# "alice> hi" + bob `send` "> @alice (hi) hey" + bob <# "@alice > hi" + bob <## " hey" + alice <# "bob> > hi" + alice <## " hey" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail @cath 1" + alice <# "@cath <- @bob" + alice <## " hey" + + cath ##> "/tail @alice 1" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardEditProhibited :: HasCallStack => FilePath -> IO () +testForwardEditProhibited = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + msgId <- lastItemId alice + alice ##> ("/_update item @3 " <> msgId <> " text hey edited") + alice <## "cannot update this item" + +testForwardDeleteForOther :: HasCallStack => FilePath -> IO () +testForwardDeleteForOther = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + + bob #> "@alice hey" + alice <# "bob> hey" + + alice `send` "@cath <- @bob hey" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hey" + + msgId <- lastItemId alice + alice ##> ("/_delete item @3 " <> msgId <> " broadcast") + alice <## "message marked deleted" + cath <# "alice> [marked deleted] hey" + +testForwardFileNoFilesFolder :: HasCallStack => FilePath -> IO () +testForwardFileNoFilesFolder = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + connectUsers alice bob + connectUsers bob cath + + -- send original file + alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice <# "@bob hi" + alice <# "/f @bob ./tests/fixtures/test.pdf" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> hi" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + + bob ##> "/fr 1 ./tests/tmp" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for bob", + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] + bob <## "completed receiving file 1 (test.pdf) from alice" + + src <- B.readFile "./tests/fixtures/test.pdf" + dest <- B.readFile "./tests/tmp/test.pdf" + dest `shouldBe` src + + -- forward file + bob `send` "@cath <- @alice hi" + bob <# "@cath <- @alice" + bob <## " hi" + bob <# "/f @cath ./tests/tmp/test.pdf" + bob <## "use /fc 2 to cancel sending" + cath <# "bob> -> forwarded" + cath <## " hi" + cath <# "bob> sends file test.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + + cath ##> "/fr 1 ./tests/tmp" + concurrentlyN_ + [ bob <## "completed uploading file 2 (test.pdf) for cath", + cath + <### [ "saving file 1 from bob to ./tests/tmp/test_1.pdf", + "started receiving file 1 (test.pdf) from bob" + ] + ] + cath <## "completed receiving file 1 (test.pdf) from bob" + + dest2 <- B.readFile "./tests/tmp/test_1.pdf" + dest2 `shouldBe` src + +testForwardFileContactToContact :: HasCallStack => FilePath -> IO () +testForwardFileContactToContact = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp" + setRelativePaths bob "./tests/tmp/bob_files" "./tests/tmp/bob_xftp" + setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp" + connectUsers alice bob + connectUsers bob cath + + -- send original file + alice ##> "/_send @2 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice <# "@bob hi" + alice <# "/f @bob test.pdf" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> hi" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + + bob ##> "/fr 1" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for bob", + bob + <### [ "saving file 1 from alice to test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] + bob <## "completed receiving file 1 (test.pdf) from alice" + + src <- B.readFile "./tests/fixtures/test.pdf" + dest <- B.readFile "./tests/tmp/bob_files/test.pdf" + dest `shouldBe` src + + -- forward file + bob `send` "@cath <- @alice hi" + bob <# "@cath <- @alice" + bob <## " hi" + bob <# "/f @cath test_1.pdf" + bob <## "use /fc 2 to cancel sending" + cath <# "bob> -> forwarded" + cath <## " hi" + cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + + cath ##> "/fr 1" + concurrentlyN_ + [ bob <## "completed uploading file 2 (test_1.pdf) for cath", + cath + <### [ "saving file 1 from bob to test_1.pdf", + "started receiving file 1 (test_1.pdf) from bob" + ] + ] + cath <## "completed receiving file 1 (test_1.pdf) from bob" + + src2 <- B.readFile "./tests/tmp/bob_files/test_1.pdf" + src2 `shouldBe` dest + dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf" + dest2 `shouldBe` src2 + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/bob_files/test.pdf" $ do + bob ##> "/clear alice" + bob <## "alice: all messages are removed locally ONLY" + fwdFileExists <- doesFileExist "./tests/tmp/bob_files/test_1.pdf" + fwdFileExists `shouldBe` True + +testForwardFileGroupToNotes :: HasCallStack => FilePath -> IO () +testForwardFileGroupToNotes = + testChat2 aliceProfile cathProfile $ + \alice cath -> withXFTPServer $ do + setRelativePaths alice "./tests/fixtures" "./tests/tmp/alice_xftp" + setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp" + createGroup2 "team" alice cath + createCCNoteFolder cath + + -- send original file + alice ##> "/_send #1 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice <# "#team hi" + alice <# "/f #team test.pdf" + alice <## "use /fc 1 to cancel sending" + cath <# "#team alice> hi" + cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + + cath ##> "/fr 1" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for #team", + cath + <### [ "saving file 1 from alice to test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] + cath <## "completed receiving file 1 (test.pdf) from alice" + + src <- B.readFile "./tests/fixtures/test.pdf" + dest <- B.readFile "./tests/tmp/cath_files/test.pdf" + dest `shouldBe` src + + -- forward file + cath `send` "* <- #team hi" + cath <# "* <- #team" + cath <## " hi" + cath <# "* file 2 (test_1.pdf)" + + dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf" + dest2 `shouldBe` dest + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/cath_files/test.pdf" $ do + cath ##> "/clear #team" + cath <## "#team: all messages are removed locally ONLY" + fwdFileExists <- doesFileExist "./tests/tmp/cath_files/test_1.pdf" + fwdFileExists `shouldBe` True + +testForwardFileNotesToGroup :: HasCallStack => FilePath -> IO () +testForwardFileNotesToGroup = + testChat2 aliceProfile cathProfile $ + \alice cath -> withXFTPServer $ do + setRelativePaths alice "./tests/tmp/alice_files" "./tests/tmp/alice_xftp" + setRelativePaths cath "./tests/tmp/cath_files" "./tests/tmp/cath_xftp" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_files/test.pdf" + createCCNoteFolder alice + createGroup2 "team" alice cath + + -- create original file + alice ##> "/_create *1 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice <# "* hi" + alice <# "* file 1 (test.pdf)" + + -- forward file + alice `send` "#team <- * hi" + alice <# "#team hi" + alice <# "/f #team test_1.pdf" + alice <## "use /fc 2 to cancel sending" + cath <# "#team alice> hi" + cath <# "#team alice> sends file test_1.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + + cath ##> "/fr 1" + concurrentlyN_ + [ alice <## "completed uploading file 2 (test_1.pdf) for #team", + cath + <### [ "saving file 1 from alice to test_1.pdf", + "started receiving file 1 (test_1.pdf) from alice" + ] + ] + cath <## "completed receiving file 1 (test_1.pdf) from alice" + + src <- B.readFile "./tests/tmp/alice_files/test.pdf" + src2 <- B.readFile "./tests/tmp/alice_files/test_1.pdf" + src2 `shouldBe` src + dest2 <- B.readFile "./tests/tmp/cath_files/test_1.pdf" + dest2 `shouldBe` src2 + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/alice_files/test.pdf" $ do + alice ##> "/clear *" + alice <## "notes: all messages are removed" + fwdFileExists <- doesFileExist "./tests/tmp/alice_files/test_1.pdf" + fwdFileExists `shouldBe` True diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 77bac11145..4d9d94e24d 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -9,15 +9,15 @@ import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) import Control.Monad (void, when) -import qualified Data.ByteString as B +import qualified Data.ByteString.Char8 as B import Data.List (isInfixOf) import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) -import Simplex.Chat.Types (GroupMemberRole (..), VersionRangeChat) +import Simplex.Chat.Types (VersionRangeChat) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB -import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOff) import System.Directory (copyFile) import System.FilePath (()) import Test.Hspec hiding (it) @@ -28,6 +28,7 @@ chatGroupTests = do describe "add contacts, create group and send/receive messages" testGroupMatrix it "v1: add contacts, create group and send/receive messages" testGroup it "v1: add contacts, create group and send/receive messages, check messages" testGroupCheckMessages + it "send large message" testGroupLargeMessage it "create group with incognito membership" testNewGroupIncognito it "create and join group with 4 members" testGroup2 it "create and delete group" testGroupDelete @@ -119,6 +120,7 @@ chatGroupTests = do it "forward file (x.msg.file.descr)" testGroupMsgForwardFile it "forward role change (x.grp.mem.role)" testGroupMsgForwardChangeRole it "forward new member announcement (x.grp.mem.new)" testGroupMsgForwardNewMember + it "forward member leaving (x.grp.leave)" testGroupMsgForwardLeave describe "group history" $ do it "text messages" testGroupHistory it "history is sent when joining via group link" testGroupHistoryGroupLink @@ -149,19 +151,19 @@ chatGroupTests = do it "member was blocked before joining group" testBlockForAllBeforeJoining it "can't repeat block, unblock" testBlockForAllCantRepeat where - _0 = supportedChatVRange PQSupportOff -- don't create direct connections + _0 = supportedChatVRange -- don't create direct connections _1 = groupCreateDirectVRange -- having host configured with older version doesn't have effect in tests -- because host uses current code and sends version in MemberInfo testNoDirect vrMem2 vrMem3 noConns = it ( "host " - <> vRangeStr (supportedChatVRange PQSupportOff) + <> vRangeStr supportedChatVRange <> (", 2nd mem " <> vRangeStr vrMem2) <> (", 3rd mem " <> vRangeStr vrMem3) <> (if noConns then " : 2 3" else " : 2 <##> 3") ) - $ testNoGroupDirectConns (supportedChatVRange PQSupportOff) vrMem2 vrMem3 noConns + $ testNoGroupDirectConns supportedChatVRange vrMem2 vrMem3 noConns testGroup :: HasCallStack => FilePath -> IO () testGroup = @@ -356,6 +358,20 @@ testGroupShared alice bob cath checkMessages directConnections = do alice #$> ("/_unread chat #1 on", id, "ok") alice #$> ("/_unread chat #1 off", id, "ok") +testGroupLargeMessage :: HasCallStack => FilePath -> IO () +testGroupLargeMessage = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + + img <- genProfileImg + let profileImage = "data:image/png;base64," <> B.unpack img + alice `send` ("/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"image\": \"" <> profileImage <> "\", \"groupPreferences\": {\"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}") + _trimmedCmd1 <- getTermLine alice + alice <## "profile image updated" + bob <## "alice updated group #team:" + bob <## "profile image updated" + testNewGroupIncognito :: HasCallStack => FilePath -> IO () testNewGroupIncognito = testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ @@ -1509,6 +1525,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile alice <## "Message reactions: on" alice <## "Voice messages: on" alice <## "Files and media: on" + alice <## "SimpleX links: on" alice <## "Recent history: on" bobAddedDan :: HasCallStack => TestCC -> IO () bobAddedDan cc = do @@ -2501,6 +2518,7 @@ testPlanHostContactDeletedGroupLinkKnown = testPlanGroupLinkOwn :: HasCallStack => FilePath -> IO () testPlanGroupLinkOwn tmp = withNewTestChatCfg tmp testCfgGroupLinkViaContact "alice" aliceProfile $ \alice -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -3591,9 +3609,9 @@ testConfigureGroupDeliveryReceipts tmp = testNoGroupDirectConns :: HasCallStack => VersionRangeChat -> VersionRangeChat -> VersionRangeChat -> Bool -> FilePath -> IO () testNoGroupDirectConns hostVRange mem2VRange mem3VRange noDirectConns tmp = - withNewTestChatCfg tmp testCfg {chatVRange = const hostVRange} "alice" aliceProfile $ \alice -> do - withNewTestChatCfg tmp testCfg {chatVRange = const mem2VRange} "bob" bobProfile $ \bob -> do - withNewTestChatCfg tmp testCfg {chatVRange = const mem3VRange} "cath" cathProfile $ \cath -> do + withNewTestChatCfg tmp testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do createGroup3 "team" alice bob cath if noDirectConns then contactsDontExist bob cath @@ -3882,6 +3900,20 @@ testMemberContactMessage = (bob <## "alice (Alice): contact is connected") bob #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")]) + + -- exchanging messages will enable PQ (see Chat "TODO PQ" - perhaps connection should be negotiated with PQ on) + alice <##> bob + alice <##> bob + + alice `send` "@bob hi" + alice <## "bob: quantum resistant end-to-end encryption enabled" + alice <# "@bob hi" + bob <## "alice: quantum resistant end-to-end encryption enabled" + bob <# "alice> hi" + + bob #> "@alice hey" + alice <# "bob> hey" + alice <##> bob -- bob and cath connect @@ -4406,6 +4438,18 @@ testGroupMsgForwardNewMember = "dan (Daniel): member" ] +testGroupMsgForwardLeave :: HasCallStack => FilePath -> IO () +testGroupMsgForwardLeave = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + setupGroupForwarding3 "team" alice bob cath + + bob ##> "/leave #team" + bob <## "#team: you left the group" + bob <## "use /d #team to delete the group" + alice <## "#team: bob left the group" + cath <## "#team: bob left the group" + testGroupHistory :: HasCallStack => FilePath -> IO () testGroupHistory = testChat3 aliceProfile bobProfile cathProfile $ @@ -5184,7 +5228,11 @@ testGroupHistoryWelcomeMessage = cath ##> "/_get chat #1 count=100" r <- chat <$> getTermLine cath - r `shouldContain` [(0, "hello"), (0, "hey!"), (0, "welcome to team")] + -- sometimes there are "connected" and feature items in between, + -- so we filter them out; `shouldContain` then checks order is correct + let expected = [(0, "hello"), (0, "hey!"), (0, "welcome to team")] + r' = filter (`elem` expected) r + r' `shouldContain` expected -- message delivery works after sending history alice #> "#team 1" diff --git a/tests/ChatTests/Local.hs b/tests/ChatTests/Local.hs index 40ebe51b83..5562d517ac 100644 --- a/tests/ChatTests/Local.hs +++ b/tests/ChatTests/Local.hs @@ -150,6 +150,7 @@ testFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do doesFileExist stored `shouldReturn` True alice ##> "/clear *" + alice <## "notes: all messages are removed" alice ##> "/fs 1" alice <## "file 1 not found" alice ##> "/tail" @@ -180,6 +181,7 @@ testOtherFiles = bob ##> "/tail *" bob <# "* test" bob ##> "/clear *" + bob <## "notes: all messages are removed" bob ##> "/tail *" bob ##> "/fs 1" bob <## "receiving file 1 (test.jpg) complete, path: test.jpg" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 7996fde3ad..a8afa05af3 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} @@ -13,7 +14,10 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import qualified Data.Text as T import Simplex.Chat.Store.Shared (createContact) -import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..)) +import Simplex.Chat.Types (ConnStatus (..), Profile (..)) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) +import Simplex.Chat.Types.UITheme +import Simplex.Chat.Types.Util (encodeJSON) import Simplex.Messaging.Encoding.String (StrEncoding (..)) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec hiding (it) @@ -68,6 +72,11 @@ chatProfileTests = do it "enable timed messages in group" testEnableTimedMessagesGroup xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally it "update multiple user preferences for multiple contacts" testUpdateMultipleUserPrefs + describe "group preferences for specific member role" $ do + it "direct messages" testGroupPrefsDirectForRole + it "files & media" testGroupPrefsFilesForRole + it "SimpleX links" testGroupPrefsSimplexLinksForRole + it "set user, contact and group UI theme" testSetUITheme testUpdateProfile :: HasCallStack => FilePath -> IO () testUpdateProfile = @@ -179,10 +188,12 @@ testMultiWordProfileNames = alice <# "#'Our Team' 'Bob James'> hi" cath <# "#'Our Team' 'Bob James'> hi" alice `send` "@'Cath Johnson' hello" - alice <## "member #'Our Team' 'Cath Johnson' does not have direct connection, creating" - alice <## "contact for member #'Our Team' 'Cath Johnson' is created" - alice <## "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'" - alice <# "@'Cath Johnson' hello" + alice + <### [ "member #'Our Team' 'Cath Johnson' does not have direct connection, creating", + "contact for member #'Our Team' 'Cath Johnson' is created", + "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'", + WithTime "@'Cath Johnson' hello" + ] cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you" cath <# "'Alice Jones'> hello" cath <## "'Alice Jones': contact is connected" @@ -300,6 +311,7 @@ testProfileLink = cc <## ("contact address: " <> cLink) cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## "quantum resistant end-to-end encryption" cc <## currentChatVRangeInfo checkAliceNoProfileLink cc = do cc ##> "/info alice" @@ -308,6 +320,7 @@ testProfileLink = cc <##. "sending messages via" cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## "quantum resistant end-to-end encryption" cc <## currentChatVRangeInfo testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO () @@ -1509,7 +1522,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ alice ##> "/_set prefs @2 {}" alice <## "your preferences for bob did not change" (bob ("/_get chat @2 count=100", chat, startFeatures) bob #$> ("/_get chat @2 count=100", chat, startFeatures) let sendVoice = "/_send @2 json {\"filePath\": \"test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}" @@ -1901,3 +1914,173 @@ testUpdateMultipleUserPrefs = testChat3 aliceProfile bobProfile cathProfile $ alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi bob"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")]) alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "hi cath"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")]) + +testGroupPrefsDirectForRole :: HasCallStack => FilePath -> IO () +testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set direct #team on owner" + alice <## "updated group preferences:" + alice <## "Direct messages: on for owners" + directForOwners bob + directForOwners cath + threadDelay 1000000 + bob ##> "@cath hello again" + bob <## "bad chat command: direct messages not allowed" + (cath "/j #team" + concurrentlyN_ + [ cath <## "#team: dan joined the group", + do + dan <## "#team: you joined the group" + dan + <### [ "#team: member alice (Alice) is connected", + "#team: member bob (Bob) is connected" + ], + do + alice <## "#team: cath added dan (Daniel) to the group (connecting...)" + alice <## "#team: new member dan is connected", + do + bob <## "#team: cath added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected" + ] + -- dan cannot send direct messages to alice (owner) + dan ##> "@alice hello alice" + dan <## "bad chat command: direct messages not allowed" + (alice hello dan" + dan <## "alice (Alice): contact is connected" + -- and now dan can too + dan #> "@alice hi alice" + alice <# "dan> hi alice" + where + directForOwners :: HasCallStack => TestCC -> IO () + directForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "Direct messages: on for owners" + +testGroupPrefsFilesForRole :: HasCallStack => FilePath -> IO () +testGroupPrefsFilesForRole = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok") + bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok") + createDirectoryIfMissing True "./tests/tmp/alice" + createDirectoryIfMissing True "./tests/tmp/bob" + copyFile "./tests/fixtures/test.txt" "./tests/tmp/alice/test1.txt" + copyFile "./tests/fixtures/test.txt" "./tests/tmp/bob/test2.txt" + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set files #team on owner" + alice <## "updated group preferences:" + alice <## "Files and media: on for owners" + filesForOwners bob + filesForOwners cath + threadDelay 1000000 + bob ##> "/f #team test2.txt" + bob <## "bad chat command: feature not allowed Files and media" + (alice "/f #team test1.txt" + alice <## "use /fc 1 to cancel sending" + alice <## "completed uploading file 1 (test1.txt) for #team" + bob <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + cath <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + where + filesForOwners :: HasCallStack => TestCC -> IO () + filesForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "Files and media: on for owners" + +testGroupPrefsSimplexLinksForRole :: HasCallStack => FilePath -> IO () +testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set links #team on owner" + alice <## "updated group preferences:" + alice <## "SimpleX links: on for owners" + linksForOwners bob + linksForOwners cath + threadDelay 1000000 + bob ##> "/c" + inv <- getInvitation bob + bob ##> ("#team " <> inv) + bob <## "bad chat command: feature not allowed SimpleX links" + (alice ("#team " <> inv) + bob <# ("#team alice> " <> inv) + cath <# ("#team alice> " <> inv) + where + linksForOwners :: HasCallStack => TestCC -> IO () + linksForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "SimpleX links: on for owners" + +testSetUITheme :: HasCallStack => FilePath -> IO () +testSetUITheme = + testChat2 aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice #$> ("/_set theme user 1 " <> theme UCMDark, id, "ok") + alice #$> ("/_set theme @2 " <> theme UCMDark, id, "ok") + alice #$> ("/_set theme #1 " <> theme UCMDark, id, "ok") + alice ##> "/u" + userInfo alice "alice (Alice)" + alice <## ("UI themes: " <> theme UCMDark) + alice ##> "/create user alice2" + userInfo alice "alice2" + alice ##> "/u alice" + userInfo alice "alice (Alice)" + alice <## ("UI themes: " <> theme UCMDark) + alice ##> "/i @bob" + contactInfo alice + alice <## ("UI themes: " <> theme UCMDark) + alice ##> "/i #team" + groupInfo alice + alice <## ("UI themes: " <> theme UCMDark) + alice #$> ("/_set theme user 1", id, "ok") + alice #$> ("/_set theme @2", id, "ok") + alice #$> ("/_set theme #1", id, "ok") + alice ##> "/u" + userInfo alice "alice (Alice)" + alice ##> "/i @bob" + contactInfo alice + alice ##> "/i #team" + groupInfo alice + where + theme cm = T.unpack $ encodeJSON UIThemeEntityOverrides {light = Nothing, dark = Just $ UIThemeEntityOverride cm Nothing defaultUIColors} + userInfo a name = do + a <## ("user profile: " <> name) + a <## "use /p to change it" + a <## "(the updated profile will be sent to all your contacts)" + contactInfo a = do + a <## "contact ID: 2" + a <## "receiving messages via: localhost" + a <## "sending messages via: localhost" + a <## "you've shared main profile with this contact" + a <## "connection not verified, use /code command to see security code" + a <## "quantum resistant end-to-end encryption" + a <## "peer chat protocol version range: (Version 1, Version 8)" + groupInfo a = do + a <## "group ID: 1" + a <## "current members: 1" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 3b0748e7d0..a3ee0d5ca8 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -30,6 +30,7 @@ import Simplex.Chat.Store.NoteFolders (createNoteFolder) import Simplex.Chat.Store.Profiles (getUserContactProfiles) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.FileTransfer.Client.Main (xftpClientCLI) import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow, withTransaction) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -86,15 +87,16 @@ ifCI xrun run d t = do skip :: String -> SpecWith a -> SpecWith a skip = before_ . pendingWith -versionTestMatrix2 :: (HasCallStack => TestCC -> TestCC -> IO ()) -> SpecWith FilePath +-- Bool is pqExpected - see testAddContact +versionTestMatrix2 :: (HasCallStack => Bool -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath versionTestMatrix2 runTest = do - it "current" $ testChat2 aliceProfile bobProfile runTest - it "prev" $ testChatCfg2 testCfgVPrev aliceProfile bobProfile runTest - it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev runTest - it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg runTest - it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile runTest - it "old to curr" $ runTestCfg2 testCfg testCfgV1 runTest - it "curr to old" $ runTestCfg2 testCfgV1 testCfg runTest + it "current" $ testChat2 aliceProfile bobProfile (runTest True) + it "prev" $ testChatCfg2 testCfgVPrev aliceProfile bobProfile (runTest False) + it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev (runTest False) + it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg (runTest False) + it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest False) + it "old to curr" $ runTestCfg2 testCfg testCfgV1 (runTest False) + it "curr to old" $ runTestCfg2 testCfgV1 testCfg (runTest False) versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath versionTestMatrix3 runTest = do @@ -118,34 +120,6 @@ runTestCfg3 aliceCfg bobCfg cathCfg runTest tmp = withNewTestChatCfg tmp cathCfg "cath" cathProfile $ \cath -> runTest alice bob cath -type PQEnabled = Bool - -pqMatrix2 :: (HasCallStack => (TestCC, PQEnabled) -> (TestCC, PQEnabled) -> IO ()) -> SpecWith FilePath -pqMatrix2 runTest = do - it "PQ: off, off" $ test False False - it "PQ: on, off" $ test False True - it "PQ: off, on" $ test True False - it "PQ: on, on" $ test True True - where - test aPQ bPQ = testChat2 aliceProfile bobProfile $ \a b -> runTest (a, aPQ) (b, bPQ) - -pqVersionTestMatrix2 :: (HasCallStack => TestCC -> TestCC -> Bool -> VersionChat -> IO ()) -> SpecWith FilePath -pqVersionTestMatrix2 runTest = do - it "current" $ testChat2 aliceProfile bobProfile (runTest' True pqEncryptionCompressionVersion) - it "prev" $ testChatCfg2 testCfgVPrev aliceProfile bobProfile (runTest' False (VersionChat 6)) - it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev (runTest' False (VersionChat 6)) - it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg (runTest' False (VersionChat 6)) - it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest' False (VersionChat 1)) - it "old to curr" $ runTestCfg2 testCfg testCfgV1 (runTest' False (VersionChat 1)) - it "curr to old" $ runTestCfg2 testCfgV1 testCfg (runTest' False (VersionChat 1)) - it "next" $ testChatCfg2 testCfgVNext aliceProfile bobProfile (runTest' True pqEncryptionCompressionVersion) - it "next to curr" $ runTestCfg2 testCfg testCfgVNext (runTest' True pqEncryptionCompressionVersion) - it "curr to next" $ runTestCfg2 testCfgVNext testCfg (runTest' True pqEncryptionCompressionVersion) - it "next to prev" $ runTestCfg2 testCfgVPrev testCfgVNext (runTest' False (VersionChat 6)) - it "prev to next" $ runTestCfg2 testCfgVNext testCfgVPrev (runTest' False (VersionChat 6)) - where - runTest' pqExpected v a b = runTest a b pqExpected v - withTestChatGroup3Connected :: HasCallStack => FilePath -> String -> (HasCallStack => TestCC -> IO a) -> IO a withTestChatGroup3Connected tmp dbPrefix action = do withTestChat tmp dbPrefix $ \cc -> do @@ -286,7 +260,7 @@ chatFeaturesF = map (\(a, _, c) -> (a, c)) chatFeatures'' chatFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)] chatFeatures'' = - [ ((0, e2eeInfoNoPQStr), Nothing, Nothing), + [ ((0, e2eeInfoPQStr), Nothing, Nothing), ((0, "Disappearing messages: allowed"), Nothing, Nothing), ((0, "Full deletion: off"), Nothing, Nothing), ((0, "Message reactions: enabled"), Nothing, Nothing), @@ -315,6 +289,7 @@ groupFeatures'' = ((0, "Message reactions: on"), Nothing, Nothing), ((0, "Voice messages: on"), Nothing, Nothing), ((0, "Files and media: on"), Nothing, Nothing), + ((0, "SimpleX links: on"), Nothing, Nothing), ((0, "Recent history: on"), Nothing, Nothing) ] @@ -706,7 +681,7 @@ checkActionDeletesFile file action = do currentChatVRangeInfo :: String currentChatVRangeInfo = - "peer chat protocol version range: " <> vRangeStr (supportedChatVRange PQSupportOff) + "peer chat protocol version range: " <> vRangeStr supportedChatVRange vRangeStr :: VersionRange v -> String vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")" @@ -721,3 +696,12 @@ linkAnotherSchema link xftpCLI :: [String] -> IO [String] xftpCLI params = lines <$> capture_ (withArgs params xftpClientCLI) + +setRelativePaths :: HasCallStack => TestCC -> String -> String -> IO () +setRelativePaths cc filesFolder tempFolder = do + cc ##> "/_stop" + cc <## "chat stopped" + cc #$> ("/_files_folder " <> filesFolder, id, "ok") + cc #$> ("/_temp_folder " <> tempFolder, id, "ok") + cc ##> "/_start" + cc <## "chat started" diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 082af825e5..c4dfa7da25 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -12,6 +12,7 @@ import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet @@ -49,7 +50,7 @@ testDhPubKey :: C.PublicKeyX448 testDhPubKey = "MEIwBQYDK2VvAzkAmKuSYeQ/m0SixPDS8Wq8VBaTS1cW+Lp0n0h4Diu+kUpR+qXx4SDJ32YGEFoGFGSbGPry5Ychr6U=" testE2ERatchetParams :: RcvE2ERatchetParamsUri 'C.X448 -testE2ERatchetParams = E2ERatchetParamsUri (supportedE2EEncryptVRange PQSupportOn) testDhPubKey testDhPubKey Nothing +testE2ERatchetParams = E2ERatchetParamsUri supportedE2EEncryptVRange testDhPubKey testDhPubKey Nothing testConnReq :: ConnectionRequestUri 'CMInvitation testConnReq = CRInvitationUri connReqData testE2ERatchetParams @@ -99,7 +100,7 @@ testChatPreferences :: Maybe Preferences testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn}, files = Nothing, fullDelete = Nothing, history = Nothing} +testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing} testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), contactLink = Nothing, preferences = testChatPreferences} @@ -131,8 +132,8 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-7\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" - ##==## ChatMessage (supportedChatVRange PQSupportOff) (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) + "{\"v\":\"1-8\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" ##==## ChatMessage @@ -241,14 +242,14 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-7\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange $ supportedChatVRange PQSupportOff, profile = testProfile} + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-7\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange $ supportedChatVRange PQSupportOff, profile = testProfile} Nothing + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} (Just MemberRestrictions {restriction = MRSBlocked}) @@ -262,8 +263,8 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-7\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange $ supportedChatVRange PQSupportOff, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile diff --git a/website/.eleventy.js b/website/.eleventy.js index 09fc7c2c44..64aedbc04c 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -394,6 +394,9 @@ module.exports = function (ty) { if (parsed.path.startsWith("../../blog")) { parsed.path = parsed.path.replace("../../blog", "/blog") } + if (parsed.path.startsWith("../PRIVACY.md")) { + parsed.path = parsed.path.replace("../PRIVACY.md", "/privacy") + } parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase() return uri.serialize(parsed) } diff --git a/website/langs/ar.json b/website/langs/ar.json index 3b74ac2f3d..7b087aab42 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -15,7 +15,7 @@ "chat-protocol": "بروتوكول الدردشة", "donate": "تبرّع", "terminal-cli": "طرفية CLI", - "terms-and-privacy-policy": "الشروط وسياسة الخصوصية", + "terms-and-privacy-policy": "سياسة الخصوصية", "hero-header": "إعادة تعريف الخصوصية", "hero-overlay-1-textlink": "لماذا تعتبر معرّفات المستخدم ضارة بالخصوصية؟", "hero-overlay-2-textlink": "كيف يعمل SimpleX؟", @@ -34,7 +34,7 @@ "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2023", "simplex-chat-protocol": "بروتوكول دردشة SimpleX", "developers": "المطورين", - "hero-subheader": "أول نظام مُراسلة
بدون معرّفات المستخدم", + "hero-subheader": "أول نظام مُراسلة
دون معرّفات مُستخدم", "hero-p-1": "تحتوي التطبيقات الأخرى على معرّفات مستخدم: Signal ،Matrix ،Session ،Briar ،Jami ،Cwtch وما إلى ذلك.
SimpleX ليس لديه، ولا حتى أرقام عشوائية.
وهذا يحسن خصوصيتك بشكل جذري.", "features": "الميزات", "hero-2-header": "أجري اتصال خاص", @@ -252,5 +252,8 @@ "hero-overlay-card-3-p-1": "Trail of Bits هي شركة رائدة في مجال الاستشارات الأمنية والتكنولوجية، ومن بين عملائها شركات التكنولوجيا الكبرى والوكالات الحكومية ومشاريع blockchain الكبرى.", "docs-dropdown-9": "التنزيلات", "please-enable-javascript": "الرجاء تفعيل جافا سكريبت (JavaScript) لرؤية رمز QR.", - "please-use-link-in-mobile-app": "يُرجى استخدام الرابط في تطبيق الجوال" + "please-use-link-in-mobile-app": "يُرجى استخدام الرابط في تطبيق الجوال", + "docs-dropdown-10": "الشفافية", + "docs-dropdown-11": "الأسئلة الأكثر شيوعًا", + "docs-dropdown-12": "الأمان" } diff --git a/website/langs/bg.json b/website/langs/bg.json index b84d369678..b70a6882e4 100644 --- a/website/langs/bg.json +++ b/website/langs/bg.json @@ -2,5 +2,36 @@ "developers": "Разработчици", "glossary": "Речник", "yes": "Да", - "no": "Не" + "no": "Не", + "home": "Дом", + "reference": "Справка", + "blog": "Блог", + "features": "Характеристика", + "why-simplex": "Защо SimpleX", + "simplex-privacy": "Поверителност на SimpleX", + "simplex-network": "SimpleX мрежа", + "simplex-explained-tab-1-text": "1. Какъв опит като потребител", + "simplex-explained-tab-2-text": "2. Как работи", + "simplex-explained-tab-3-text": "3. Какво виждат сървърите", + "simplex-explained-tab-1-p-1": "Можете да създавате контакти и групи и да провеждате двупосочни разговори, както във всеки друго чат приложение.", + "simplex-explained-tab-2-p-1": "За всяка връзка използвате две отделни опашки за съобщения, за да изпращате и получавате съобщения през различни сървъри.", + "simplex-explained": "Обяснението на SimpleX", + "simplex-explained-tab-2-p-2": "Сървърите предават съобщения само еднопосочно, без да имат картина от разговора или връзките на потребителя.", + "chat-bot-example": "Пример за чат бот", + "smp-protocol": "СМП Протокол", + "chat-protocol": "Чат протокол", + "donate": "Дарете", + "copyright-label": "© 2020-2023 SimpleX | Проект с отворен код", + "simplex-chat-protocol": "SimpleX Чат протокол", + "terminal-cli": "Системна конзола", + "terms-and-privacy-policy": "Условия и политика за поверителност", + "hero-header": "Предефинирана поверителност", + "hero-overlay-1-textlink": "Защо потребителските идентификатори са лоши за поверителността?", + "hero-overlay-2-textlink": "Как работи SimpleX?", + "hero-2-header": "Осъществяване на частна връзка", + "simplex-explained-tab-1-p-2": "Как може да работи с еднопосочни опашки и без идентификатори на потребителски профили?", + "simplex-explained-tab-3-p-2": "Потребителите могат допълнително да подобрят поверителността на метаданните, като използват Tor за достъп до сървъри, предотвратявайки корелация по IP адрес.", + "hero-2-header-desc": "Видеото показва как се свързвате с приятеля си чрез неговия еднократен QR-код лично или чрез видео връзка. Можете също да се свържете, като споделите покана за връзка.", + "hero-overlay-3-textlink": "Оценка на сигурността", + "hero-overlay-1-title": "Как работи SimpleX?" } diff --git a/website/langs/cs.json b/website/langs/cs.json index 6e2416e4a2..b8777eea38 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -252,5 +252,6 @@ "jobs": "Připojit k týmu", "hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022.", "hero-overlay-card-3-p-3": "Přečtěte si více v ohlášení.", - "docs-dropdown-9": "Ke stažení" + "docs-dropdown-9": "Ke stažení", + "docs-dropdown-10": "Transparentnost" } diff --git a/website/langs/de.json b/website/langs/de.json index 7b37869429..c6bcc9920f 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -25,7 +25,7 @@ "chat-protocol": "Chat Protokoll", "simplex-chat-protocol": "SimpleX Chat Protokoll", "terminal-cli": "Terminal Kommandozeilen-Schnittstelle", - "terms-and-privacy-policy": "Bedingungen & Datenschutzbestimmungen", + "terms-and-privacy-policy": "Datenschutzrichtlinie", "hero-header": "Privatsphäre neu definiert", "hero-overlay-2-textlink": "Wie funktioniert SimpleX?", "hero-subheader": "Der erste Messenger
ohne Nutzerkennungen", @@ -252,5 +252,8 @@ "hero-overlay-card-3-p-1": "Trail of Bits ist eine führende Security- und Technologie-Unternehmensberatung, deren Kunden aus den Bereichen Big-Tech, Regierungsbehörden und großen Blockchain-Projekten stammen.", "docs-dropdown-9": "Downloads", "please-enable-javascript": "Bitte aktivieren Sie JavaScript, um den QR-Code zu sehen.", - "please-use-link-in-mobile-app": "Bitte nutzen Sie den Link in der Mobiltelefon-App" + "please-use-link-in-mobile-app": "Bitte nutzen Sie den Link in der Mobiltelefon-App", + "docs-dropdown-10": "Transparent", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Sicherheit" } diff --git a/website/langs/en.json b/website/langs/en.json index 10db2dd4ed..e606fb9bf2 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -24,7 +24,7 @@ "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Terms & Privacy Policy", + "terms-and-privacy-policy": "Privacy Policy", "hero-header": "Privacy redefined", "hero-subheader": "The first messenger
without user IDs", "hero-p-1": "Other apps have user IDs: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.
SimpleX does not, not even random numbers.
This radically improves your privacy.", @@ -235,6 +235,9 @@ "docs-dropdown-7": "Translate SimpleX Chat", "docs-dropdown-8": "SimpleX Directory Service", "docs-dropdown-9": "Downloads", + "docs-dropdown-10": "Transparency", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Security", "newer-version-of-eng-msg": "There is a newer version of this page in English.", "click-to-see": "Click to see", "menu": "Menu", diff --git a/website/langs/es.json b/website/langs/es.json index d235a030fb..d8f9c29c26 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -252,5 +252,6 @@ "hero-overlay-card-3-p-1": "Trail of Bits es una consultora de seguridad y tecnología líder cuyos clientes incluyen grandes tecnológicas, agencias gubernamentales e importantes proyectos de blockchain.", "docs-dropdown-9": "Descargas", "please-enable-javascript": "Habilita JavaScript para ver el código QR.", - "please-use-link-in-mobile-app": "Usa el enlace en la apliación móvil," + "please-use-link-in-mobile-app": "Usa el enlace en la apliación móvil,", + "docs-dropdown-10": "Transparencia" } diff --git a/website/langs/fr.json b/website/langs/fr.json index 580f4134cd..7ead7882db 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -24,7 +24,7 @@ "copyright-label": "© 2020-2023 SimpleX | Projet Open-Source", "simplex-chat-protocol": "Protocole SimpleX Chat", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Conditions et politique de confidentialité", + "terms-and-privacy-policy": "Politique de confidentialité", "hero-header": "La vie privée redéfinie", "hero-subheader": "La première messagerie
sans identifiant d'utilisateur", "hero-p-1": "Les autres applications ont des IDs d'utilisateur : Signal, Matrix, Session, Briar, Jami, Cwtch, etc.
SimpleX n'en a pas, pas même des nombres aléatoires.
Ce qui améliore radicalement votre vie privée.", @@ -253,5 +253,8 @@ "hero-overlay-card-3-p-1": "Trail of Bits est un cabinet leader dans le secteur de la sécurité et des technologies qui compte parmi ses clients des grandes entreprises de la tech, des agences gouvernementales et d'importants projets de blockchain.", "docs-dropdown-9": "Téléchargements", "please-enable-javascript": "Veuillez activer JavaScript pour voir le code QR.", - "please-use-link-in-mobile-app": "Veuillez utiliser le lien dans l'application mobile" + "please-use-link-in-mobile-app": "Veuillez utiliser le lien dans l'application mobile", + "docs-dropdown-10": "Transparence", + "docs-dropdown-12": "Sécurité", + "docs-dropdown-11": "FAQ" } diff --git a/website/langs/it.json b/website/langs/it.json index 388f461374..c2425f93fd 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -13,7 +13,7 @@ "copyright-label": "© 2020-2023 SimpleX | Progetto Open-Source", "simplex-chat-protocol": "Protocollo di SimpleX Chat", "terminal-cli": "Terminale CLI", - "terms-and-privacy-policy": "Termini e Informativa sulla privacy", + "terms-and-privacy-policy": "Informativa sulla privacy", "hero-header": "Privacy ridefinita", "hero-subheader": "Il primo messenger
senza ID utente", "hero-p-1": "Le altre app hanno gli ID utente: Signal, Matrix, Session, Briar, Jami, Cwtch, ecc.
SimpleX invece no, neanche dei numeri casuali.
Ciò aumenta radicalmente la tua privacy.", @@ -252,5 +252,8 @@ "hero-overlay-card-3-p-1": "Trail of Bits è leader nella consulenza di sicurezza e tecnologia, i cui clienti includono grandi aziende, agenzie governative e importanti progetti di blockchain.", "docs-dropdown-9": "Download", "please-enable-javascript": "Attiva JavaScript per vedere il codice QR.", - "please-use-link-in-mobile-app": "Usa il link nell'app mobile" + "please-use-link-in-mobile-app": "Usa il link nell'app mobile", + "docs-dropdown-10": "Trasparenza", + "docs-dropdown-12": "Sicurezza", + "docs-dropdown-11": "Domande frequenti" } diff --git a/website/langs/ja.json b/website/langs/ja.json index 8fa5cac1f9..3ac040272f 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -32,7 +32,7 @@ "simplex-explained-tab-1-p-2": "ユーザー プロファイル識別子なしで単方向キューをどのように処理できるのでしょうか?", "simplex-chat-protocol": "SimpleX チャットプロトコル", "terminal-cli": "ターミナル CLI", - "terms-and-privacy-policy": "利用規約とプライバシーポリシー", + "terms-and-privacy-policy": "プライバシーポリシー", "hero-header": "プライバシーの基準を新境地に", "hero-subheader": "
ユーザーIDを持たない最初のメッセンジャー", "hero-overlay-1-textlink": "ユーザー ID がプライバシーに悪影響を与えるのはなぜですか?", @@ -252,5 +252,8 @@ "hero-overlay-card-3-p-2": "Trail of Bitsは2022年11月にSimpleXプラットフォームの暗号とネットワークのコンポーネントを検証しました。", "docs-dropdown-9": "ダウンロード", "please-enable-javascript": "QRコードを表示するためにJavaScriptを有効にしてください。", - "please-use-link-in-mobile-app": "このリンクをモバイルアプリで使用してください" + "please-use-link-in-mobile-app": "このリンクをモバイルアプリで使用してください", + "docs-dropdown-10": "透明性", + "docs-dropdown-11": "よくある質問", + "docs-dropdown-12": "セキュリティ" } diff --git a/website/langs/nl.json b/website/langs/nl.json index 847baf4c3a..15729fbfe9 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -20,7 +20,7 @@ "copyright-label": "© 2020-2023 SimpleX | Open-sourceproject", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Voorwaarden & Privacybeleid", + "terms-and-privacy-policy": "Privacybeleid", "hero-subheader": "De eerste messenger
zonder gebruikers-ID's", "hero-overlay-1-textlink": "Waarom zijn gebruikers-ID's slecht voor de privacy?", "hero-overlay-2-textlink": "Hoe werkt SimpleX?", @@ -252,5 +252,8 @@ "hero-overlay-card-3-p-1": "Trail of Bits is een toonaangevend beveiligings- en technologieadviesbureau met klanten onder meer grote technologiebedrijven, overheidsinstanties en grote blockchain-projecten.", "docs-dropdown-9": "Downloads", "please-enable-javascript": "Schakel JavaScript in om de QR-code te zien.", - "please-use-link-in-mobile-app": "Gebruik de link in de mobiele app" + "please-use-link-in-mobile-app": "Gebruik de link in de mobiele app", + "docs-dropdown-10": "Transparantie", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Beveiliging" } diff --git a/website/langs/pl.json b/website/langs/pl.json index 3e7c546cf1..bbfb2dd9b7 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -18,7 +18,7 @@ "copyright-label": "© 2020-2023 SimpleX | Projekt Open-Source", "simplex-chat-protocol": "Protokół SimpleX Chat", "terminal-cli": "Terminal CLI", - "terms-and-privacy-policy": "Warunki i polityka prywatności", + "terms-and-privacy-policy": "Polityka prywatności", "hero-header": "Prywatność zdefiniowana na nowo", "hero-subheader": "Pierwszy komunikator
bez identyfikatorów użytkowników (ID)", "hero-overlay-1-textlink": "Dlaczego identyfikatory użytkowników (ID) są złe dla prywatności?", @@ -252,5 +252,8 @@ "hero-overlay-card-3-p-1": "Trail of Bits jest wiodącą firmą konsultingową w zakresie bezpieczeństwa i technologii, której klientami są duże firmy technologiczne, agencje rządowe i główne projekty blockchain.", "docs-dropdown-9": "Pliki do pobrania", "please-enable-javascript": "Prosimy o włączenie JavaScript w celu wyświetlenia kodu QR.", - "please-use-link-in-mobile-app": "Prosimy o skorzystanie z linku w aplikacji mobilnej" + "please-use-link-in-mobile-app": "Prosimy o skorzystanie z linku w aplikacji mobilnej", + "docs-dropdown-10": "Przezroczystość", + "docs-dropdown-12": "Bezpieczeństwo", + "docs-dropdown-11": "Często zadawane pytania" } diff --git a/website/langs/ru.json b/website/langs/ru.json index 1629aafe54..4a9100c76d 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -235,7 +235,7 @@ "privacy-matters-overlay-card-1-p-4": "Платформа SimpleX защищает конфиденциальность ваших контактов лучше, чем любая другая альтернатива, полностью предотвращая доступ к вашему социальному графику каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.", "hero-overlay-card-1-p-6": "Подробнее читайте в техническом документе SimpleX.", "simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями P2P имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.", - "terms-and-privacy-policy": "Условия & Политика Конфиденциальности", + "terms-and-privacy-policy": "Политика Конфиденциальности", "simplex-network-overlay-card-1-li-1": "Сети P2P полагаются на тот или иной вариант DHT для маршрутизации сообщений. Проекты DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети - O(log N).", "privacy-matters-section-label": "Убедитесь, что ваш мессенджер не может получить доступ к вашим данным!", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в портативном формате зашифрованной базы данных которую можно перенести на другое устройство.", @@ -252,5 +252,7 @@ "f-droid-page-f-droid-org-repo-section-text": "Приложение SimpleX Chat от разработчиков и от репозитория F-Droid.org имеют разные ключи подписи. Если вы хотите сменить одно на другое, вам сначала нужно будет экспортировать базу данных и только потом скачать другое приложение.", "simplex-private-5-title": "Многоуровневое
Заполнения содержимого", "please-use-link-in-mobile-app": "Пожалуйста, воспользуйтесь ссылкой в мобильном приложении", - "please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код." + "please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.", + "docs-dropdown-10": "Прозрачность", + "docs-dropdown-12": "Безопасность" } diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index ded7c7e19b..a81f4d123c 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -1,7 +1,7 @@ { "simplex-explained-tab-3-p-2": "用户可以通过使用 Tor 访问服务器以进一步提高元数据隐私,防止通过 IP 地址关联实际身份。", "chat-bot-example": "聊天机器人示例", - "terms-and-privacy-policy": "条款和隐私政策", + "terms-and-privacy-policy": "隐私政策", "hero-overlay-1-textlink": "为什么用户 ID 不利于隐私?", "hero-2-header": "建立私人连接", "hero-overlay-2-title": "为什么用户 ID 不利于隐私?", @@ -252,5 +252,8 @@ "hero-overlay-card-3-p-1": "Trail of Bits 是一家领先的安全和技术咨询企业,其客户包括大型科技公司、政府机构和重要的区块链项目。", "docs-dropdown-9": "下载", "please-use-link-in-mobile-app": "请使用移动应用程序中的链接", - "please-enable-javascript": "请启用 JavaScript 以查看二维码。" + "please-enable-javascript": "请启用 JavaScript 以查看二维码。", + "docs-dropdown-10": "透明度", + "docs-dropdown-11": "常问问题", + "docs-dropdown-12": "安全性" } diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json index a8c6e634b2..88fd5826dd 100644 --- a/website/src/_data/docs_dropdown.json +++ b/website/src/_data/docs_dropdown.json @@ -35,6 +35,18 @@ { "title": "docs-dropdown-9", "url": "/downloads/" + }, + { + "title": "docs-dropdown-10", + "url": "/transparency/" + }, + { + "title": "docs-dropdown-12", + "url": "/security/" + }, + { + "title": "docs-dropdown-11", + "url": "/faq/" } ] } \ No newline at end of file diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index 857684661b..e9ccb7ce02 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -26,7 +26,10 @@ "TRANSLATIONS.md", "WEBRTC.md", "XFTP-SERVER.md", - "DOWNLOADS.md" + "DOWNLOADS.md", + "TRANSPARENCY.md", + "SECURITY.md", + "FAQ.md" ] }, { diff --git a/website/src/_includes/blog_previews/20240416.html b/website/src/_includes/blog_previews/20240416.html new file mode 100644 index 0000000000..6c6edfb6c1 --- /dev/null +++ b/website/src/_includes/blog_previews/20240416.html @@ -0,0 +1,5 @@ +By Esra'a al Shafei + +

It's important not to be complacent with the current standards of messaging, + where metadata aggregation is still normalized in apps falsely and dangerously marketed as "private". + This is a post exploring the fundamental differences between privacy and security.

\ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240426.html b/website/src/_includes/blog_previews/20240426.html new file mode 100644 index 0000000000..648bcc0744 --- /dev/null +++ b/website/src/_includes/blog_previews/20240426.html @@ -0,0 +1,16 @@ +

Legally binding transparency

+ +
    +
  • We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy.
  • +
  • We are committed to have full consistency between marketing promises and legally binding documents.
  • +
+ +

v5.7 is released:

+ +
    +
  • quantum resistant end-to-end encryption with all contacts.
  • +
  • forward and save messages without revealing the source.
  • +
  • in-call sounds and switching sound sources.
  • +
  • better network connection management.
  • +
  • customizable profile images.
  • +
diff --git a/website/src/_includes/footer.html b/website/src/_includes/footer.html index 1c23806760..0cc1b16f8d 100644 --- a/website/src/_includes/footer.html +++ b/website/src/_includes/footer.html @@ -20,11 +20,9 @@ {{ "simplex-chat-protocol" | i18n({}, lang ) | safe }} - {{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }} - + + + + + + + SimpleX Privacy Policy + + + + + + {% if image %} + + {% else %} + + {% endif %} + + + + + + + + + + {% include "navbar.html" %} + +
+
{{ content | safe }}
+
+ + {% include "footer.html" %} + + + diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 55836bd576..5f9d4be3f3 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -13,7 +13,7 @@
-