diff --git a/PRIVACY.md b/PRIVACY.md index 18e5539726..5713f8e134 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -27,17 +27,17 @@ permalink: /privacy/index.html SimpleX Chat (also referred to as SimpleX) 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 messaging 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 apps allow their users to send messages and files via relay server infrastructure. Relay server owners and operators 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 host user accounts. +SimpleX messaging 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 apps allow their users to send messages and files via relay server infrastructure. Relay server owners and operators do not have any access to your messages, thanks to quantum-resistant 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 host 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)). -If you believe that any part of this document is not aligned with SimpleX network mission or values, please raise it via [email](mailto: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). +If you believe that any part of this document is not aligned with SimpleX network mission or values, please raise it via [email](mailto:chat@simplex.chat) or [chat](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw). ## Privacy Policy ### General principles -SimpleX network software 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 is protected from being 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). +SimpleX network software uses established industry practices for security and encryption to provide secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption is protected from [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack) by the relay servers, even if they are modified or compromised. SimpleX 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 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. @@ -45,7 +45,7 @@ SimpleX software is similar in its design approach to email clients and browsers SimpleX network operators are not communication service provider, and provide public relays "as is", as experimental, without any guarantees of availability or data retention. The operators of the relay servers preset in the app ("Preset Server Operators"), including SimpleX Chat Ltd, are committed to maintain a high level of availability, reliability and security. SimpleX client apps can have multiple preset relay server operators that you can opt-in or opt-out of using. You are and will continue to be able to use any other operators or your own servers. -SimpleX network design is based on the principles of users and data sovereignty, and device and operator portability. +SimpleX network design is based on the principles of user and data sovereignty, and device and operator portability. The implementation security assessment of SimpleX cryptography and networking 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). @@ -69,22 +69,26 @@ Your message history is stored only on your own device and the devices of your c #### Private message delivery -You do not have control over which servers are used to send messages to your contacts - these servers are chosen by your contacts. To send messages your client by default uses configured servers to forward messages to the destination servers, thus protecting your IP address from the servers chosen by your contacts. +You do not have control over which servers are used to receive messages by your contacts - these servers are chosen by your contacts. To send messages your client by default uses configured servers to forward messages to the destination servers, thus protecting your IP address from the servers chosen by your contacts. In case you use preset servers of more than one operator, the app will prefer to use a server of an operator different from the operator of the destination server to forward messages, preventing destination server to correlate messages as belonging to one client. +Preset servers do not log IP addresses of the user devices that connect to them. + You can additionally use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by you. *Please note*: the clients allow changing configuration to connect to the destination servers directly. It is not recommended - if you make such change, your IP address will be visible to the destination servers. #### Storage of messages and files on the servers -The messages are removed from the relay servers as soon as all messages of the file they were stored in are delivered and saving new messages switches to another file, as long as these servers use unmodified published code. Undelivered messages are also marked as delivered after the time that is configured in the messaging servers you use (21 days for preset messaging servers). +The messages stored on the servers are end-to-end encrypted, and cannot be read by server owners. + +The messages are irreversibly removed from the preset relay servers as soon as they are delivered or after 21 days. + +Other relay servers may use message logs that would result in longer storage of delivered messages, until the log file is rotated, which normally should happen within one month if servers use the same code as preset servers. The files are stored on file relay servers for the time configured in the relay servers you use (48 hours for preset file servers). -The encrypted messages can be stored for some time after they are delivered or expired (because servers use append-only logs for message storage). This time varies, and may be longer in connections with fewer messages, but it is usually limited to 1 month, including any backup storage. - #### Connections with other users When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers, or, if available, the relays of two different operators, for increased privacy, in case you have more than one relay server configured in the app, which is the default. @@ -93,15 +97,11 @@ Preset and unmodified SimpleX relay servers do not store information about which #### Connection links privacy -When you create a connection with another user, the app generates a link/QR code that can be shared with the user to establish the connection via any channel (email, any other messenger, or a video call). This link is safe to share via insecure channels, as long as you can identify the recipient and also trust that this channel did not replace this link (to mitigate the latter risk you can validate the security code via the app). +When you create a connection with another user, the app generates a one-time link/QR code that can be shared with the user to establish the connection via any channel (email, any other messenger, or a video call). This link is safe to share via insecure channels, as long as you can identify the recipient and also trust that this channel did not replace this link (to mitigate the latter risk you can validate the security code via the app). -While the connection "links" contain SimpleX Chat Ltd domain name `simplex.chat`, this site is never accessed by the app, and is only used for these purposes: -- to direct the new users to the app download instructions, -- to show connection QR code that can be scanned via the app, -- to "namespace" these links, -- to open links directly in the installed app when it is clicked outside of the app. +The connection link contains the address of the server used to establish the connection. Your profile name and picture are stored on this server in encrypted form until your contact uses the link, after which this data is removed. The server cannot access this data without the link. -You can always safely replace the initial part of the link `https://simplex.chat/` either with `simplex:/` (which is a URI scheme provisionally registered with IANA) or with any other domain name where you can self-host the app download instructions and show the connection QR code (but in case it is your domain, it will not open in the app). Also, while the page renders QR code, all the information needed to render it is only available to the browser, as the part of the "link" after `#` symbol is not sent to the website server. +The old connection "links" contained SimpleX Chat Ltd domain name `simplex.chat`, but this site is never accessed by the app - you could replace the initial part of the old link `https://simplex.chat/` either with `simplex:/` or with any other domain name. #### iOS Push Notifications @@ -117,11 +117,15 @@ You can read more about the design of iOS push notifications [here](./blog/20220 Additional technical information can be stored on the network servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX network design limits this additional technical information to the minimum required to operate the software and servers. To prevent server overloading or attacks, the servers can temporarily store data that can link to particular users or devices, including IP addresses, geographic location, or information related to the transport sessions. This information is not stored for the absolute majority of the app users, even for those who use the servers very actively. +Because SimpleX servers do not create user accounts and do not store any identifiers linking transport data to message queues or user profiles, this technical data cannot be used by server operators to identify any person. + #### SimpleX Directory -This section applies only to the experimental group directory operated by SimpleX Chat Ltd. +This section applies only to the experimental group directory chat bot operated by SimpleX Chat Ltd. -[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) bot stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok). + +You can also view the groups registered in SimpleX directory via the browser at [simplex.chat/directory](https://simplex.chat/directory) #### Public groups and content channels @@ -131,15 +135,23 @@ You may participate in a public group and receive content from a public channel - to retain a copy of your messages according to the Group settings (e.g., the Group may allow irreversible message deletion from the recipient devices for a limited period of time, or it may only allow to edit and mark messages as deleted on recipient devices). Deleting message from the recipient devices or marking message as deleted revokes the license to share the message. - to Group owners: to share your messages with the new Group members as history of the Group. Currently, the Group history shared with the new members is limited to 100 messages. -Group owners may use chat relays or automated bots (Chat Relays) to re-broadcast member messages to all members, for efficiency. The Chat Relays may be operated by the group owners, by preset operators or by 3rd parties. The Chat Relays have access to and will retain messages in line with Group settings, for technical functioning of the Group. Neither you nor group owners grant any content license to Chat Relay operators. +#### Public channels and chat relays (beta) + +Public channels are experimental - their functionality and privacy properties may change. + +Channel owners use chat relays that retain messages to deliver them to channel subscribers. The chat relays may be operated by the channel owners, by preset operators or by 3rd parties. The chat relays are client applications on SimpleX network - they cannot identify subscribers. Neither you nor channel owners grant any content license to chat relay operators. + +#### Public contact, group and channel addresses + +Public addresses contain profile name, picture and other profile details. This data is encrypted on the servers, and can only be accessed via the address. Server operators cannot list addresses and cannot access this data without having the address. Public address data remains on the servers until removed by the user via the app. If you lose access to the app without a backup, server operators have no way to verify address ownership and can only remove addresses following due process. #### User Support -The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via 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) when it is possible, and avoid sharing any personal information. +The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw) when it is possible, and avoid sharing any personal information. ### Preset Server Operators -Preset server operators will not share the information on their servers with each other, other than aggregate usage statistics. +Preset server operators will not share the information or any metadata on their servers with each other, other than aggregate usage statistics. Preset server operators must not provide general access to their servers or the data on their servers to each other. @@ -149,7 +161,7 @@ Preset server operators will provide non-administrative access to control port o The preset server operators use third parties. While they do not have access and cannot share any user data, these third parties may access the encrypted user messages (but NOT the actual unencrypted message content or size) as it is stored or transmitted via the servers. Hosting and network providers can also store IP addresses and other transport information as part of their logs. -SimpleX Chat Ltd uses a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, please contact us via SimpleX Chat apps or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat). +SimpleX Chat Ltd uses a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, please contact us via SimpleX Chat apps or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/vks/v1/by-fingerprint/FB44AF81A45BDE327319797C85107E357D4A17FC). The cases when the preset server operators may share the data temporarily stored on the servers: @@ -158,9 +170,7 @@ The cases when the preset server operators may share the data temporarily stored - To detect, prevent, or otherwise address fraud, security, or technical issues. - To protect against harm to the rights, property, or safety of software users, operators of preset servers, or the public as required or permitted by law. -By the time of updating this document, the preset server operators were not served with any enforceable requests and did not provide any information from the servers to any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law. - -Preset server operators will publish information they are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). +Reports on requests for user data are published in [Transparency reports](./docs/TRANSPARENCY.md). To date, no user information was provided in response to any requests. If the preset server operators are ever required to provide information, they will follow the due legal process to limit any information shared to the minimally required by law. ### Source code license @@ -168,6 +178,8 @@ As this software is fully open-source and provided under AGPLv3 license, all inf In addition to the AGPLv3 license terms, the preset relay server operators are committed to the software users that these servers will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications. +Users can independently [reproduce builds](./docs/REPRODUCE.md) to verify that the published client and server binaries were compiled from the published code. + ### Updates This Privacy Policy applies to SimpleX Chat Ltd and all other preset server operators you use in the app. @@ -176,7 +188,7 @@ This Privacy Policy may be updated as needed so that it is current, accurate, an Please also read The Conditions of Use of Software and Infrastructure below. -If you have questions about this Privacy Policy please contact SimpleX Chat Ltd via [email](mailto: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). +If you have questions about this Privacy Policy or data protection please contact SimpleX Chat Ltd (company number 13691484, registered at 20-22 Wenlock Road, London, United Kingdom N1 7GU) via [email](mailto:chat@simplex.chat) or [chat](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw). ## Conditions of Use of Software and Infrastructure @@ -188,6 +200,8 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b **Client applications**. SimpleX Chat client application Software (referred to as "app" or "apps") also has no modifications compared with published open-source code, and any developers of the alternative client apps based on SimpleX Chat code are required to publish any modifications under the same AGPLv3 license. Client applications should not include any tracking or analytics code, and do not share any tracking information with SimpleX Chat Ltd, preset server operators or any other third parties. If you ever discover any tracking or analytics code, please report it to SimpleX Chat Ltd, so it can be removed. +Client applications must not include any code that could compromise the security of end-to-end encryption of files and messages. Client applications must not send anything not directly required for users communications without explicit users' consent. + **Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to the [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about the privacy model and known security and privacy risks. **Privacy of user data**. Servers do not retain any data you transmit for any longer than necessary to deliver the messages between apps. Preset server operators collect aggregate statistics across all their servers, as supported by published code and can be enabled by any infrastructure operator, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. SimpleX Chat Ltd does not have information about how many people use SimpleX Chat applications, it only knows an approximate number of app installations and the aggregate traffic through the preset servers. In any case, preset server operators do not and will not sell or in any way monetize user data. The future business model assumes charging for some optional Software features instead, in a transparent and fair way. @@ -243,4 +257,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 and preset server operators at any time by deleting the Applications from your devices and discontinuing use of the Infrastructure of SimpleX Chat Ltd and preset server operators. 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 and/or preset server operators. -Updated March 3, 2025 +Updated April 18, 2026 diff --git a/apps/ios/.gitignore b/apps/ios/.gitignore index 3d152a0610..ea8e911891 100644 --- a/apps/ios/.gitignore +++ b/apps/ios/.gitignore @@ -69,3 +69,7 @@ Libraries/ Shared/MyPlayground.playground/* testpush.sh + +# Local build config and generated assets +Local.xcconfig +Shared/SimpleXAssets.xcassets/*.imageset diff --git a/apps/ios/Debug.xcconfig b/apps/ios/Debug.xcconfig new file mode 100644 index 0000000000..7f0389c760 --- /dev/null +++ b/apps/ios/Debug.xcconfig @@ -0,0 +1,2 @@ +SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; +#include? "Local.xcconfig" diff --git a/apps/ios/README.md b/apps/ios/README.md index fb6a6ed40d..1e987f655e 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -85,6 +85,27 @@ Workflow: - `Product > Export Localizations` - Export XLIFF files - `Product > Import Localizations` - Import updated translations +## SimpleX Assets + +The app includes optional assets behind the `SIMPLEX_ASSETS` Swift compilation flag. Without setup, the app builds normally without them. + +### Setup + +Create `Local.xcconfig` (gitignored) in the `apps/ios/` directory: +``` +SIMPLEX_ASSETS_DIR = /path/to/assets +SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) SIMPLEX_ASSETS +``` + +The copy script (`scripts/ios/copy-assets.sh`) runs as a build phase on each build but exits immediately if `SIMPLEX_ASSETS` is not set. + +### Updating assets + +When source images change, regenerate resized images (requires ImageMagick): +```bash +cd path/to/assets && ./resize.sh +``` + ## Background Capabilities Configured in Info.plist: diff --git a/apps/ios/Release.xcconfig b/apps/ios/Release.xcconfig new file mode 100644 index 0000000000..234f81e782 --- /dev/null +++ b/apps/ios/Release.xcconfig @@ -0,0 +1 @@ +#include? "Local.xcconfig" diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 1131069d88..c93cc233f5 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -62,6 +62,7 @@ enum ChatCommand: ChatCmdProtocol { case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64]) case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) + case apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -128,7 +129,7 @@ enum ChatCommand: ChatCmdProtocol { case apiAddContact(userId: Int64, incognito: Bool) case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiChangeConnectionUser(connId: Int64, userId: Int64) - case apiConnectPlan(userId: Int64, connLink: String) + case apiConnectPlan(userId: Int64, connLink: String, linkOwnerSig: LinkOwnerSig?) case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData) case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) @@ -261,6 +262,9 @@ enum ChatCommand: ChatCmdProtocol { let ttlStr = ttl != nil ? "\(ttl!)" : "default" let asGroup = sendAsGroup ? " as_group=on" : "" return "/_forward \(ref(toChatType, toChatId, scope: toScope))\(asGroup) \(ref(fromChatType, fromChatId, scope: fromScope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" + case let .apiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup): + let asGroup = sendAsGroup ? "(as_group=on)" : "" + return "/_share chat content \(ref(shareChatType, shareChatId, scope: nil)) \(ref(toChatType, toChatId, scope: toScope))\(asGroup)" 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)" @@ -337,7 +341,9 @@ enum ChatCommand: ChatCmdProtocol { case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" - case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)" + case let .apiConnectPlan(userId, connLink, linkOwnerSig): + let sigStr = if let linkOwnerSig { " sig=\(encodeJSON(linkOwnerSig))" } else { "" } + return "/_connect plan \(userId) \(connLink)\(sigStr)" case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))" case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") direct=\(onOff(directLink)) \(encodeJSON(groupShortLinkData))" case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)" @@ -451,6 +457,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiGetReactionMembers: return "apiGetReactionMembers" case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" case .apiForwardChatItems: return "apiForwardChatItems" + case .apiShareChatMsgContent: return "apiShareChatMsgContent" case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" @@ -821,6 +828,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case acceptingContactRequest(user: UserRef, contact: Contact) case contactRequestRejected(user: UserRef, contactRequest: UserContactRequest, contact_: Contact?) case newChatItems(user: UserRef, chatItems: [AChatItem]) + case chatMsgContent(user: UserRef, msgContent: MsgContent) case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set, byUser: Bool, member_: GroupMember?) case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?) case chatItemUpdated(user: UserRef, chatItem: AChatItem) @@ -864,6 +872,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case .acceptingContactRequest: "acceptingContactRequest" case .contactRequestRejected: "contactRequestRejected" case .newChatItems: "newChatItems" + case .chatMsgContent: "chatMsgContent" case .groupChatItemsDeleted: "groupChatItemsDeleted" case .forwardPlan: "forwardPlan" case .chatItemUpdated: "chatItemUpdated" @@ -898,6 +907,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case let .newChatItems(u, chatItems): let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") return withUser(u, itemsString) + case let .chatMsgContent(u, mc): return withUser(u, String(describing: mc)) case let .groupChatItemsDeleted(u, gInfo, chatItemIDs, byUser, member_): return withUser(u, "chatItemIDs: \(String(describing: chatItemIDs))\ngroupInfo: \(String(describing: gInfo))\nbyUser: \(byUser)\nmember_: \(String(describing: member_))") case let .forwardPlan(u, chatItemIds, forwardConfirmation): return withUser(u, "items: \(chatItemIds) forwardConfirmation: \(String(describing: forwardConfirmation))") @@ -932,6 +942,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { // group responses case groupCreated(user: UserRef, groupInfo: GroupInfo) case publicGroupCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink, groupRelays: [GroupRelay]) + case publicGroupCreationFailed(user: UserRef, addRelayResults: [AddRelayResult]) case groupRelays(user: UserRef, groupInfo: GroupInfo, groupRelays: [GroupRelay]) case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember) case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) @@ -984,6 +995,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { switch self { case .groupCreated: "groupCreated" case .publicGroupCreated: "publicGroupCreated" + case .publicGroupCreationFailed: "publicGroupCreationFailed" case .groupRelays: "groupRelays" case .sentGroupInvitation: "sentGroupInvitation" case .userAcceptedGroupSent: "userAcceptedGroupSent" @@ -1032,6 +1044,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { switch self { case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .publicGroupCreated(u, groupInfo, groupLink, groupRelays): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)\ngroupRelays: \(groupRelays)") + case let .publicGroupCreationFailed(u, addRelayResults): return withUser(u, "addRelayResults: \(addRelayResults)") case let .groupRelays(u, groupInfo, groupRelays): return withUser(u, "groupInfo: \(groupInfo)\ngroupRelays: \(groupRelays)") case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)") case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))") @@ -1341,6 +1354,11 @@ enum ChatPagination { } } +enum OwnerVerification: Decodable, Hashable { + case verified + case failed(reason: String) +} + enum ConnectionPlan: Decodable, Hashable { case invitationLink(invitationLinkPlan: InvitationLinkPlan) case contactAddress(contactAddressPlan: ContactAddressPlan) @@ -1349,14 +1367,14 @@ enum ConnectionPlan: Decodable, Hashable { } enum InvitationLinkPlan: Decodable, Hashable { - case ok(contactSLinkData_: ContactShortLinkData?) + case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?) case ownLink case connecting(contact_: Contact?) case known(contact: Contact) } enum ContactAddressPlan: Decodable, Hashable { - case ok(contactSLinkData_: ContactShortLinkData?) + case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?) case ownLink case connectingConfirmReconnect case connectingProhibit(contact: Contact) @@ -1371,11 +1389,12 @@ public struct GroupShortLinkInfo: Decodable, Hashable { } enum GroupLinkPlan: Decodable, Hashable { - case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?) + case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?) case ownLink(groupInfo: GroupInfo) case connectingConfirmReconnect case connectingProhibit(groupInfo_: GroupInfo?) case known(groupInfo: GroupInfo) + case noRelays(groupSLinkData_: GroupShortLinkData?) } struct ChatTagData: Encodable { @@ -1965,6 +1984,11 @@ struct RelayConnectionResult: Decodable { var relayError: ChatError? } +struct AddRelayResult: Decodable { + var relay: UserChatRelay + var relayError: ChatError? +} + enum ProtocolTestStep: String, Decodable, Equatable { case connect case disconnect diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e527df1abd..20653ab9db 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -458,7 +458,7 @@ func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTa func apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope? = nil) async throws -> [MsgContentTag] { let r: ChatResponse0 = try await chatSendCmd(.apiGetChatContentTypes(chatId: chatId, scope: scope)) - if case let .chatContentTypes(types) = r { return types } + if case let .chatContentTypes(types) = r { return types.filter { if case .unknown = $0 { return false }; return true } } throw r.unexpected } @@ -503,6 +503,12 @@ func apiPlanForwardChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, throw r.unexpected } +func apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) async throws -> MsgContent { + let r: ChatResponse1 = try await chatSendCmd(.apiShareChatMsgContent(shareChatType: shareChatType, shareChatId: shareChatId, toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup)) + if case let .chatMsgContent(_, mc) = r { return mc } + throw r.unexpected +} + func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool = false, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) @@ -1020,12 +1026,12 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi if let r { throw r.unexpected } else { return nil } } -func apiConnectPlan(connLink: String, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { +func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectPlan: no current user") return (nil, nil) } - let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink), inProgress: inProgress) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, linkOwnerSig: linkOwnerSig), inProgress: inProgress) if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } return (nil, alert) @@ -1111,6 +1117,27 @@ private func apiConnectResponseAlert(_ r: APIResult) -> Alert { } } +func connErrorText(_ e: ChatError) -> String { + switch e { + case .error(.invalidConnReq): + NSLocalizedString("Invalid connection link", comment: "conn error description") + case .error(.unsupportedConnReq): + NSLocalizedString("Unsupported connection link", comment: "conn error description") + case .errorAgent(.SMP(_, .AUTH)): + NSLocalizedString("Connection error (AUTH)", comment: "conn error description") + case let .errorAgent(.SMP(_, .BLOCKED(info))): + NSLocalizedString("Connection blocked: \(info.reason.text)", comment: "conn error description") + case .errorAgent(.SMP(_, .QUOTA)): + NSLocalizedString("The connection reached the limit of undelivered messages", comment: "conn error description") + default: + if getNetworkErrorAlert(e) != nil { + NSLocalizedString("Network error", comment: "conn error description") + } else { + "\(NSLocalizedString("Error", comment: "conn error description")): \(responseError(e))" + } + } +} + func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { mkAlert( title: "Contact already exists", @@ -1841,12 +1868,19 @@ func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInf throw r.unexpected } -func apiNewPublicGroup(incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) async throws -> (GroupInfo, GroupLink, [GroupRelay])? { +enum PublicGroupCreationResult { + case created(GroupInfo, GroupLink, [GroupRelay]) + case creationFailed([AddRelayResult]) +} + +func apiNewPublicGroup(incognito: Bool, relayIds: [Int64], groupProfile: GroupProfile) async throws -> PublicGroupCreationResult? { let userId = try currentUserId("apiNewPublicGroup") let r: APIResult? = await chatApiSendCmdWithRetry(.apiNewPublicGroup(userId: userId, incognito: incognito, relayIds: relayIds, groupProfile: groupProfile)) switch r { case let .result(.publicGroupCreated(_, groupInfo, groupLink, groupRelays)): - return (groupInfo, groupLink, groupRelays) + return .created(groupInfo, groupLink, groupRelays) + case let .result(.publicGroupCreationFailed(_, addRelayResults)): + return .creationFailed(addRelayResults) default: if let r { throw r.unexpected } else { return nil } } } diff --git a/apps/ios/Shared/SimpleXAssets.xcassets/Contents.json b/apps/ios/Shared/SimpleXAssets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/Shared/SimpleXAssets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift new file mode 100644 index 0000000000..aaaa29929d --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatLinkHeader.swift @@ -0,0 +1,68 @@ +import SwiftUI +import SimpleXChat + +struct CIChatLinkHeader: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool + var chatItem: ChatItem + var chatLink: MsgChatLink + var ownerSig: LinkOwnerSig? + var hasText: Bool + + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + + var body: some View { + VStack(alignment: .leading) { + linkProfileView() + .padding(.horizontal, 2) + .padding(.top, 8) + .padding(.bottom, 6) + .overlay(DetermineWidth()) + Divider() + VStack(alignment: .leading, spacing: 2) { + if let descr = chatLink.shortDescription { + Text(descr) + .font(.footnote) + .foregroundColor(theme.colors.secondary) + .lineLimit(2) + .padding(.bottom, 2) + } + Text(chatLink.infoLine(signed: ownerSig != nil)) + .font(.footnote) + .foregroundColor(theme.colors.secondary) + .padding(.bottom, 2) + let t = Text("Tap to open").foregroundColor(theme.colors.primary).font(.callout) + if hasText { + t + } else { + t + + Text(verbatim: " ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) + } + } + .overlay(DetermineWidth()) + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + } + + private func linkProfileView() -> some View { + HStack(alignment: .top) { + ProfileImage( + imageStr: chatLink.image, + iconName: chatLink.iconName, + size: 44, + color: Color(uiColor: .tertiaryLabel) + ) + .padding(.trailing, 4) + VStack(alignment: .leading) { + Text(chatLink.displayName).font(.headline).lineLimit(2) + let fn = chatLink.fullName + if fn != "" && fn != chatLink.displayName { + Text(fn).font(.subheadline).lineLimit(2) + } + } + .frame(minHeight: 44) + } + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index ec8bc852c0..112aa33c31 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -169,6 +169,16 @@ struct FramedItemView: View { case let .link(_, preview): CILinkView(linkPreview: preview) ciMsgContentView(chatItem) + case let .chat(text, chatLink, ownerSig): + let hasText = text != chatLink.connLinkStr + CIChatLinkHeader(chatItem: chatItem, chatLink: chatLink, ownerSig: ownerSig, hasText: hasText) + .overlay(DetermineWidth()) + .simultaneousGesture(TapGesture().onEnded { + planAndConnect(chatLink.connLinkStr, linkOwnerSig: ownerSig, theme: theme, dismiss: false) + }) + if hasText { + ciMsgContentView(chatItem, stripLink: chatLink.connLinkStr) + } case let .unknown(_, text: text): if chatItem.file == nil { ciMsgContentView(chatItem) @@ -244,6 +254,11 @@ struct FramedItemView: View { ciQuotedMsgView(qi) .padding(.trailing, 20).frame(minWidth: msgWidth, alignment: .leading) ciQuoteIconView("mic.fill") + case let .chat(text, chatLink, _): + let prefix = NSAttributedString(string: chatLink.displayName + (text != chatLink.connLinkStr ? " - " : "")) + ciQuotedMsgView(qi, stripLink: chatLink.connLinkStr, prefix: prefix) + .padding(.trailing, 20).frame(minWidth: msgWidth, alignment: .leading) + ciQuoteIconView(chatLink.smallIconName) default: ciQuotedMsgView(qi) } @@ -260,7 +275,7 @@ struct FramedItemView: View { } } - private func ciQuotedMsgView(_ qi: CIQuote) -> some View { + private func ciQuotedMsgView(_ qi: CIQuote, stripLink: String? = nil, prefix: NSAttributedString? = nil) -> some View { Group { if let sender = qi.getSender(membership()) { VStack(alignment: .leading, spacing: 2) { @@ -268,10 +283,10 @@ struct FramedItemView: View { .font(.caption) .foregroundColor(qi.chatDir == .groupSnd ? .accentColor : theme.colors.secondary) .lineLimit(1) - ciQuotedMsgTextView(qi, lines: 2) + ciQuotedMsgTextView(qi, lines: 2, stripLink: stripLink, prefix: prefix) } } else { - ciQuotedMsgTextView(qi, lines: 3) + ciQuotedMsgTextView(qi, lines: 3, stripLink: stripLink, prefix: prefix) } } .fixedSize(horizontal: false, vertical: true) @@ -280,8 +295,8 @@ struct FramedItemView: View { } @inline(__always) - private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View { - MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline) + private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int, stripLink: String? = nil, prefix: NSAttributedString? = nil) -> some View { + MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, textStyle: .subheadline, prefix: prefix, stripLink: stripLink) .lineLimit(lines) .padding(.bottom, 6) } @@ -303,7 +318,7 @@ struct FramedItemView: View { } } - @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, txtPrefix: NSAttributedString? = nil) -> some View { + @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, txtPrefix: NSAttributedString? = nil, stripLink: String? = nil) -> some View { let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text let rtl = isRightToLeft(text) let ft = text == "" ? [] : ci.formattedText @@ -316,7 +331,8 @@ struct FramedItemView: View { mentions: ci.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, rightToLeft: rtl, - prefix: txtPrefix + prefix: txtPrefix, + stripLink: stripLink ) .environment(\.containerBackground, UIColor(chatItemFrameColor(ci, theme))) .multilineTextAlignment(rtl ? .trailing : .leading) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index c816770c76..d831333c20 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -84,12 +84,13 @@ struct RcvMsgErrorItemView: View { var body: some View { CIMsgError(chat: chat, chatItem: chatItem) { - AlertManager.shared.showAlertMsg( - title: "Message error", - message: switch rcvMsgError { + let message: LocalizedStringKey = switch rcvMsgError { case let .dropped(attempts): "The app removed this message after \(attempts) attempts to receive it." case let .parseError(parseError): "\(parseError)" } + AlertManager.shared.showAlertMsg( + title: "Message error", + message: message ) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 77bd41c5b8..2f4338c0af 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -39,6 +39,7 @@ struct MsgContentView: View { var userMemberId: String? = nil var rightToLeft = false var prefix: NSAttributedString? = nil + var stripLink: String? = nil @State private var showSecrets: Set = [] @State private var typingIdx = 0 @State private var timer: Timer? @@ -105,7 +106,8 @@ struct MsgContentView: View { showSecrets: showSecrets, commands: chat.chatInfo.useCommands && chat.chatInfo.sndReady, backgroundColor: containerBackground, - prefix: prefix + prefix: prefix, + stripLink: stripLink ) let s = r.string let t: Text @@ -289,8 +291,11 @@ func messageText( showSecrets: Set?, commands: Bool = false, backgroundColor: UIColor, - prefix: NSAttributedString? = nil + prefix: NSAttributedString? = nil, + stripLink: String? = nil ) -> MsgTextResult { + let text = if let stripLink { stripTextLink(text, stripLink) } else { text } + let formattedText = if let stripLink { stripFormattedTextLink(formattedText, stripLink) } else { formattedText } let res = NSMutableAttributedString() let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) let font = UIFont.preferredFont(forTextStyle: textStyle) @@ -465,6 +470,24 @@ func viaHost(_ smpHosts: [String]) -> String { "(via \(smpHosts.first ?? "?"))" } +func stripTextLink(_ text: String, _ link: String) -> String { + text == link + ? "" + : text.hasSuffix("\n" + link) + ? String(text.dropLast(link.count + 1)) + : text +} + +func stripFormattedTextLink(_ ft: [FormattedText]?, _ link: String) -> [FormattedText]? { + guard var ft, ft.last?.text == link else { return ft } + ft.removeLast() + if let i = ft.indices.last, ft[i].format == nil, ft[i].text.hasSuffix("\n") { + ft[i].text = String(ft[i].text.dropLast()) + if ft[i].text.isEmpty { ft.removeLast() } + } + return ft.isEmpty ? nil : ft +} + struct MsgContentView_Previews: PreviewProvider { static var previews: some View { let chatItem = ChatItem.getSample(1, .directSnd, .now, "hello") diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index dfc620c402..d83a5e8504 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -14,9 +14,12 @@ struct ChatItemForwardingView: View { @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss - var chatItems: [ChatItem] - var fromChatInfo: ChatInfo - @Binding var composeState: ComposeState + var title: LocalizedStringKey = "Forward" + var chatItems: [ChatItem] = [] + var fromChatInfo: ChatInfo? = nil + var composeState: Binding? = nil + var isProhibited: ((Chat) -> Bool)? = nil + var onSelectChat: ((Chat) -> Void)? = nil @State private var searchText: String = "" @State private var alert: SomeAlert? @@ -32,7 +35,7 @@ struct ChatItemForwardingView: View { } } ToolbarItem(placement: .principal) { - Text("Forward") + Text(title) .bold() } } @@ -71,7 +74,7 @@ struct ChatItemForwardingView: View { } @ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View { - let prohibited = chatItems.map { ci in + let prohibited = isProhibited?(chat) ?? chatItems.map { ci in chat.prohibitedByPref( hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text), isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false, @@ -88,16 +91,19 @@ struct ChatItemForwardingView: View { ), id: "forward prohibited by preferences" ) - } else { + } else if let onSelectChat { + dismiss() + onSelectChat(chat) + } else if let fromChatInfo, let composeState { dismiss() if chat.id == fromChatInfo.id { - composeState = ComposeState( - message: composeState.message, - preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, + composeState.wrappedValue = ComposeState( + message: composeState.wrappedValue.message, + preview: composeState.wrappedValue.linkPreview != nil ? composeState.wrappedValue.preview : .noPreview, contextItem: .forwardingItems(chatItems: chatItems, fromChatInfo: fromChatInfo) ) } else { - composeState = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo) + composeState.wrappedValue = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo) ItemsModel.shared.loadOpenChat(chat.id) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 1898fd2851..e165c01710 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -141,8 +141,7 @@ struct ChatView: View { showCommandsMenu: $showCommandsMenu, keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, - selectedRange: $selectedRange, - disabledText: chat.chatInfo.userCantSendReason?.composeLabel + selectedRange: $selectedRange ) } else { SelectedItemsBottomToolbar( @@ -261,7 +260,9 @@ struct ChatView: View { groupLinkMemberRole: $groupLinkMemberRole, showTitle: true, creatingGroup: false, - isChannel: groupInfo.useRelays + isChannel: groupInfo.useRelays, + groupInfo: groupInfo, + composeState: $composeState ) } } @@ -512,6 +513,7 @@ struct ChatView: View { } ), scrollToItemId: $scrollToItemId, + composeState: $composeState, onSearch: { focusSearch() }, localAlias: groupInfo.localAlias ) @@ -2109,7 +2111,7 @@ struct ChatView: View { func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading let live = composeState.liveMessage != nil - let canReply = ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote && selectedChatItems == nil + let canReply = ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote && selectedChatItems == nil && chat.chatInfo.sendMsgEnabled return ZStack(alignment: .trailing) { Image(systemName: "arrowshape.turn.up.left") .font(.system(size: 18)) @@ -2277,7 +2279,7 @@ struct ChatView: View { availableReactions.count > 0 { reactionsGroup } - if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote { + if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote && chat.chatInfo.sendMsgEnabled { replyButton } let fileSource = getLoadedFileSource(ci.file) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift new file mode 100644 index 0000000000..650ea8a87f --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeChatLinkView.swift @@ -0,0 +1,42 @@ +import SwiftUI +import SimpleXChat + +struct ComposeChatLinkView: View { + @EnvironmentObject var theme: AppTheme + var chatLink: MsgChatLink + var cancelPreview: () -> Void + let cancelEnabled: Bool + + var body: some View { + HStack(alignment: .center, spacing: 8) { + ProfileImage( + imageStr: chatLink.image, + iconName: chatLink.iconName, + size: 44 + ) + .padding(.leading, 12) + VStack(alignment: .leading, spacing: 2) { + Text(chatLink.displayName) + .font(.headline) + .lineLimit(1) + if let descr = chatLink.shortDescription { + Text(descr) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .lineLimit(1) + } + } + .padding(.vertical, 5) + Spacer() + if cancelEnabled { + Button { cancelPreview() } label: { + Image(systemName: "multiply") + } + } + } + .padding(.vertical, 8) + .padding(.trailing, 12) + .background(theme.appColors.sentMessage) + .frame(maxWidth: .infinity) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index f37eb614b9..fd47ddfacb 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -11,6 +11,7 @@ let MAX_NUMBER_OF_MENTIONS = 3 enum ComposePreview { case noPreview case linkPreview(linkPreview: LinkPreview?) + case chatLinkPreview(chatLink: MsgChatLink, ownerSig: LinkOwnerSig?) case mediaPreviews(mediaPreviews: [(String, UploadContent?)]) case voicePreview(recordingFileName: String, duration: Int) case filePreview(fileName: String, file: URL) @@ -73,7 +74,11 @@ struct ComposeState { } init(editingItem: ChatItem) { - let text = editingItem.content.text + let text = if case let .chat(t, chatLink, _) = editingItem.content.msgContent { + stripTextLink(t, chatLink.connLinkStr) + } else { + editingItem.content.text + } self.message = text self.parsedMessage = editingItem.formattedText ?? FormattedText.plain(text) self.preview = chatItemPreview(chatItem: editingItem) @@ -172,6 +177,7 @@ struct ComposeState { switch preview { case let .mediaPreviews(media): return !media.isEmpty case .voicePreview: return voiceMessageRecordingState == .finished + case .chatLinkPreview: return true case .filePreview: return true default: return !whitespaceOnly || forwarding || liveMessage != nil || submittingValidReport } @@ -183,6 +189,7 @@ struct ComposeState { var linkPreviewAllowed: Bool { switch preview { + case .chatLinkPreview: return false case .mediaPreviews: return false case .voicePreview: return false case .filePreview: return false @@ -238,6 +245,7 @@ struct ComposeState { switch preview { case .noPreview: false case .linkPreview: false + case .chatLinkPreview: false case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty case .voicePreview: false case .filePreview: true @@ -336,7 +344,6 @@ struct ComposeView: View { @Binding var keyboardVisible: Bool @Binding var keyboardHiddenDate: Date @Binding var selectedRange: NSRange - var disabledText: LocalizedStringKey? = nil @State var linkUrl: String? = nil @State var hasSimplexLink: Bool = false @@ -363,6 +370,8 @@ struct ComposeView: View { @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false + @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true + @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, store: groupDefaults) private var linkPreviewsShowAlert = true @State private var updatingCompose = false @State private var relayListExpanded = false @StateObject private var channelRelaysModel = ChannelRelaysModel.shared @@ -382,15 +391,12 @@ struct ComposeView: View { Divider() } + let ownerState = ownerRelayState if let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, ![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) { if gInfo.membership.memberRole == .owner { - let relays = channelRelaysModel.groupId == gInfo.groupId - ? channelRelaysModel.groupRelays : [] - let failedCount = relays.filter { relayMemberConnFailed($0) != nil }.count - let activeCount = relays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count - if !relays.isEmpty && activeCount < relays.count { - ownerChannelRelayBar(relays: relays, activeCount: activeCount, failedCount: failedCount) + if let s = ownerState, s.activeCount < s.relays.count { + ownerChannelRelayBar(relays: s.relays, activeCount: s.activeCount, failedCount: s.failedCount, removedCount: s.removedCount) } } else { let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted() @@ -398,18 +404,18 @@ struct ComposeView: View { .filter { $0.wrapped.memberRole == .relay } .sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") } let showProgress = !gInfo.nextConnectPrepared || composeState.inProgress - let connectedCount = relayMembers.filter { $0.wrapped.activeConn?.connStatus == .ready }.count - let deletedCount = relayMembers.filter { $0.wrapped.activeConn?.connStatus == .deleted }.count - let failedCount = relayMembers.filter { $0.wrapped.activeConn?.connFailedErr != nil }.count - let errorCount = deletedCount + failedCount - let resolvedCount = connectedCount + deletedCount + let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count + let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count + let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count + let resolvedCount = connectedCount + removedCount + failedCount let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count - if total > 0, !showProgress || resolvedCount < total { + if total > 0, removedCount + failedCount > 0 || resolvedCount < total { subscriberChannelRelayBar( hostnames: hostnames, relayMembers: relayMembers, connectedCount: connectedCount, - errorCount: errorCount, + removedCount: removedCount, + failedCount: failedCount, total: total, showProgress: showProgress ) @@ -417,8 +423,9 @@ struct ComposeView: View { } } + let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: ownerState?.noActiveRelays ?? false) let composeEnabled = ( - chat.chatInfo.sendMsgEnabled || + userCantSendReason == nil || (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) || (chat.chatInfo.contact?.nextAcceptContactRequest ?? false) ) @@ -521,7 +528,7 @@ struct ComposeView: View { .disabled(!composeEnabled) .if(!composeEnabled) { v in v.onTapGesture { - if let reason = chat.chatInfo.userCantSendReason { + if let reason = userCantSendReason { AlertManager.shared.showAlertMsg( title: "You can't send messages!", message: reason.alertMessage @@ -556,7 +563,7 @@ struct ComposeView: View { } else { composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) } - if composeState.linkPreviewAllowed && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) { + if composeState.linkPreviewAllowed && useLinkPreviews { if !msg.isEmpty { showLinkPreview(parsedMsg) } else { @@ -723,35 +730,90 @@ struct ComposeView: View { } } - private func ownerChannelRelayBar(relays: [GroupRelay], activeCount: Int, failedCount: Int) -> some View { + private var ownerRelayState: (relays: [GroupRelay], activeCount: Int, failedCount: Int, removedCount: Int, noActiveRelays: Bool)? { + guard let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, + gInfo.membership.memberRole == .owner, + ![.memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) + else { return nil } + let relays = channelRelaysModel.groupId == gInfo.groupId + ? channelRelaysModel.groupRelays : [] + guard !relays.isEmpty else { return nil } + let relayMembers = relays.map { relay in + (relay, chatModel.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })?.wrapped) + } + let removedCount = relayMembers.filter { (_, m) in relayMemberRemoved(m?.memberStatus) }.count + let activeCount = relayMembers.filter { (relay, m) in !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == .rsActive && m?.activeConn?.connFailedErr == nil }.count + let failedCount = relayMembers.filter { (_, m) in !relayMemberRemoved(m?.memberStatus) && m?.activeConn?.connFailedErr != nil }.count + let noActiveRelays = activeCount == 0 && (failedCount + removedCount) == relays.count + return (relays, activeCount, failedCount, removedCount, noActiveRelays) + } + + private var disabledText: LocalizedStringKey? { + chat.chatInfo.userCantSendReason(allRelaysBroken: ownerRelayState?.noActiveRelays ?? false)?.composeLabel + } + + @ViewBuilder private func ownerChannelRelayBar(relays: [GroupRelay], activeCount: Int, failedCount: Int, removedCount: Int) -> some View { let total = relays.count - let sorted = relays.sorted { relayDisplayName($0) < relayDisplayName($1) } - return VStack(spacing: 0) { + let allBroken = activeCount == 0 && (failedCount + removedCount) == total + let sorted = relays.map { relay in + (relay, chatModel.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })?.wrapped) + }.sorted { relayDisplayName($0.0) < relayDisplayName($1.0) } + VStack(spacing: 0) { relayBarHeader { - if activeCount + failedCount < total { + if !allBroken && activeCount + failedCount + removedCount < total { RelayProgressIndicator(active: activeCount, total: total) } - if failedCount > 0 { - Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d failed", comment: "channel relay bar progress with errors"), activeCount, total, failedCount)) + if allBroken { + if removedCount == total { + Text("All relays removed") + } else if failedCount == total { + Text("All relays failed") + } else { + Text("No active relays") + } + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + } else if activeCount + failedCount + removedCount >= total { + if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays not active", comment: "channel relay bar"), failedCount + removedCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays failed", comment: "channel relay bar"), failedCount)) + } else { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays removed", comment: "channel relay bar"), removedCount)) + } + } else if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d errors", comment: "channel relay bar"), activeCount, total, failedCount + removedCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d failed", comment: "channel relay bar"), activeCount, total, failedCount)) + } else if removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active, %d removed", comment: "channel relay bar"), activeCount, total, removedCount)) } else { Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays active", comment: "channel relay bar progress"), activeCount, total)) } } if relayListExpanded { - ForEach(sorted) { relay in - let failedErr = relayMemberConnFailed(relay) - if let err = failedErr { + if allBroken { + Text("Adding relays will be supported later.") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .padding(.leading, 12) + .padding(.trailing) + .padding(.bottom, 4) + } + ForEach(sorted, id: \.0.id) { (relay, m) in + if let err = m?.activeConn?.connFailedErr { Button { showAlert( NSLocalizedString("Relay connection failed", comment: "alert title"), message: err ) } label: { - ownerRelayDetailRow(relay, connFailed: true) + ownerRelayDetailRow(relay, connFailed: true, memberStatus: m?.memberStatus) } .buttonStyle(.plain) } else { - ownerRelayDetailRow(relay, connFailed: false) + ownerRelayDetailRow(relay, connFailed: false, memberStatus: m?.memberStatus) } } } @@ -760,38 +822,70 @@ struct ComposeView: View { .animation(nil, value: relayListExpanded) } - private func ownerRelayDetailRow(_ relay: GroupRelay, connFailed: Bool) -> some View { + private func ownerRelayDetailRow(_ relay: GroupRelay, connFailed: Bool, memberStatus: GroupMemberStatus?) -> some View { relayBarDetailRow { Text(relayDisplayName(relay)).foregroundColor(theme.colors.secondary) Spacer() - relayStatusIndicator(relay.relayStatus, connFailed: connFailed) + relayStatusIndicator(relay.relayStatus, connFailed: connFailed, memberStatus: memberStatus) } } - private func subscriberChannelRelayBar( + @ViewBuilder private func subscriberChannelRelayBar( hostnames: [String], relayMembers: [GMember], connectedCount: Int, - errorCount: Int, + removedCount: Int, + failedCount: Int, total: Int, showProgress: Bool ) -> some View { + let errorCount = removedCount + failedCount + let allBroken = connectedCount == 0 && errorCount == total VStack(spacing: 0) { relayBarHeader { - if showProgress && connectedCount + errorCount < total { - RelayProgressIndicator(active: connectedCount, total: total) - } - if showProgress { - if errorCount > 0 { - Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d errors", comment: "channel subscriber relay bar progress with errors"), connectedCount, total, errorCount)) + if allBroken { + if removedCount == total { + Text("All relays removed") + } else if failedCount == total { + Text("All relays failed") + } else { + Text("No active relays") + } + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + } else { + if showProgress && connectedCount + errorCount < total { + RelayProgressIndicator(active: connectedCount, total: total) + } + if connectedCount + removedCount + failedCount >= total, removedCount + failedCount > 0 { + if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays not active", comment: "channel subscriber relay bar"), failedCount + removedCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays failed", comment: "channel subscriber relay bar"), failedCount)) + } else if removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d relays removed", comment: "channel subscriber relay bar"), removedCount)) + } + } else if failedCount > 0 && removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d errors", comment: "channel subscriber relay bar"), connectedCount, total, errorCount)) + } else if failedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d failed", comment: "channel subscriber relay bar"), connectedCount, total, failedCount)) + } else if removedCount > 0 { + Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected, %d removed", comment: "channel subscriber relay bar"), connectedCount, total, removedCount)) } else { Text(String.localizedStringWithFormat(NSLocalizedString("%d/%d relays connected", comment: "channel subscriber relay bar progress"), connectedCount, total)) } - } else { - Text(String.localizedStringWithFormat(NSLocalizedString("%d relays", comment: "channel relay bar"), total)) } } if relayListExpanded { + if allBroken { + Text("Waiting for channel owner to add relays.") + .frame(maxWidth: .infinity, alignment: .leading) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .padding(.leading, 12) + .padding(.trailing) + .padding(.bottom, 4) + } if relayMembers.isEmpty { ForEach(hostnames, id: \.self) { relay in relayBarDetailRow { @@ -875,9 +969,9 @@ struct ComposeView: View { .padding(.vertical, 2) } - private func relayMemberConnFailed(_ relay: GroupRelay) -> String? { - chatModel.groupMembers.first(where: { $0.wrapped.groupMemberId == relay.groupMemberId })? - .wrapped.activeConn?.connFailedErr + + private func relayMemberRemoved(_ status: GroupMemberStatus?) -> Bool { + status.map { [.memLeft, .memRemoved, .memGroupDeleted].contains($0) } ?? false } private func connectButtonView(_ label: LocalizedStringKey, icon: String, connect: @escaping () -> Void) -> some View { @@ -920,7 +1014,7 @@ struct ComposeView: View { private func sendMessageView(_ disableSendButton: Bool, placeholder: String? = nil, sendToConnect: (() -> Void)? = nil) -> some View { ZStack(alignment: .leading) { SendMessageView( - placeholder: placeholder, + placeholder: disabledText != nil ? nil : placeholder, composeState: $composeState, selectedRange: $selectedRange, sendMessage: { ttl in @@ -1216,6 +1310,15 @@ struct ComposeView: View { cancelEnabled: !composeState.inProgress ) Divider() + case let .chatLinkPreview(chatLink, _): + ComposeChatLinkView( + chatLink: chatLink, + cancelPreview: { + composeState = composeState.copy(preview: .noPreview) + }, + cancelEnabled: !composeState.inProgress + ) + Divider() case let .mediaPreviews(mediaPreviews: media): ComposeImageView( images: media.map { (img, _) in img }, @@ -1374,6 +1477,10 @@ struct ComposeView: View { sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl, mentions: mentions) case .linkPreview: sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + case let .chatLinkPreview(chatLink, ownerSig): + let linkStr = chatLink.connLinkStr + let text = msgText.isEmpty ? linkStr : msgText + "\n" + linkStr + sent = await send(.chat(text: text, chatLink: chatLink, ownerSig: ownerSig), quoted: quoted, live: live, ttl: ttl, mentions: mentions) case let .mediaPreviews(media): // TODO: CHECK THIS let last = media.count - 1 @@ -1476,9 +1583,9 @@ struct ComposeView: View { return .file(msgText) case .report(_, let reason): return .report(text: msgText, reason: reason) - // TODO [short links] update chat link - case let .chat(_, chatLink): - return .chat(text: msgText, chatLink: chatLink) + case let .chat(_, chatLink, ownerSig): + let text = msgText.isEmpty ? chatLink.connLinkStr : msgText + "\n" + chatLink.connLinkStr + return .chat(text: text, chatLink: chatLink, ownerSig: ownerSig) case .unknown(let type, _): return .unknown(type: type, text: msgText) } @@ -1773,21 +1880,55 @@ struct ComposeView: View { // Spec: spec/client/compose.md#loadLinkPreview private func loadLinkPreview(_ urlStr: String) { if pendingLinkUrl == urlStr, let url = URL(string: urlStr) { - composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) - getLinkPreview(url: url) { linkPreview in - if let linkPreview, pendingLinkUrl == urlStr { - privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 - composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - composeState = composeState.copy(preview: .noPreview) + if linkPreviewsShowAlert { + showLinkPreviewsConfirmAlert { enable in + if let enable { + linkPreviewsShowAlert = false + useLinkPreviews = enable + UserDefaults.standard.set(enable, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + if enable { + fetchLinkPreview(url, urlStr: urlStr) + } else { + pendingLinkUrl = nil + composeState = composeState.copy(preview: .noPreview) + } + } else { + cancelLinkPreview() } } - pendingLinkUrl = nil + return } + fetchLinkPreview(url, urlStr: urlStr) } } + private func fetchLinkPreview(_ url: URL, urlStr: String) { + composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) + getLinkPreview(url: url) { linkPreview in + if let linkPreview, pendingLinkUrl == urlStr { + composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + composeState = composeState.copy(preview: .noPreview) + } + } + pendingLinkUrl = nil + } + } + + private func showLinkPreviewsConfirmAlert(onChoice: @escaping (Bool?) -> Void) { + showAlert( + NSLocalizedString("Enable link previews?", comment: "alert title"), + message: NSLocalizedString("Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later.", comment: "alert message"), + actions: { + [ + UIAlertAction(title: NSLocalizedString("Disable", comment: "alert button"), style: .destructive) { _ in onChoice(false) }, + UIAlertAction(title: NSLocalizedString("Enable", comment: "alert button"), style: .default) { _ in onChoice(true) } + ] + } + ) + } + private func resetLinkPreview() { linkUrl = nil prevLinkUrl = nil diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 845442c75f..1f328b2061 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -71,7 +71,7 @@ struct ContextItemView: View { } private func contextMsgPreview(_ contextItem: ChatItem) -> some View { - let r = messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(background)) + let r = messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(background), stripLink: contextItem.content.msgContent?.chatLinkStr) let t = attachment() + Text(AttributedString(r.string)) return t.if(r.hasSecrets, transform: hiddenSecretsView) @@ -83,6 +83,9 @@ struct ContextItemView: View { case .file: return isFileLoaded ? image("doc.fill") : Text("") case .image: return image("photo") case .voice: return isFileLoaded ? image("play.fill") : Text("") + case let .chat(_, chatLink, _): + let hasText = contextItem.text != chatLink.connLinkStr + return image(chatLink.smallIconName) + Text(chatLink.displayName) + Text(verbatim: hasText ? " - " : "") default: return Text("") } } diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift index 1a4e384e24..c88d639199 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift @@ -73,7 +73,9 @@ struct ChannelRelaysView: View { } private func ownerRelayStatusText(_ member: GroupMember) -> LocalizedStringKey { - if case .failed = member.activeConn?.connStatus { + if [.memLeft, .memRemoved, .memGroupDeleted].contains(member.memberStatus) { + relayConnStatus(member).text + } else if case .failed = member.activeConn?.connStatus { "failed" } else if member.activeConn?.connDisabled ?? false { "disabled" @@ -104,11 +106,16 @@ struct ChannelRelaysView: View { } func relayConnStatus(_ member: GroupMember) -> (text: LocalizedStringKey, color: Color) { - switch member.activeConn?.connStatus { - case .ready: ("connected", .green) - case .deleted: ("deleted", .red) - case .failed: ("failed", .red) - default: ("connecting", .yellow) + switch member.memberStatus { + case .memLeft: ("removed by operator", .red) + case .memRemoved, .memGroupDeleted: (member.memberStatus.text, .red) + default: + switch member.activeConn?.connStatus { + case .ready: ("connected", .green) + case .deleted: ("deleted", .red) + case .failed: ("failed", .red) + default: ("connecting", .yellow) + } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index c02f4dae36..9279c53c83 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -20,8 +20,10 @@ struct GroupChatInfoView: View { @ObservedObject var chat: Chat @Binding var groupInfo: GroupInfo @Binding var scrollToItemId: ChatItem.ID? + @Binding var composeState: ComposeState var onSearch: () -> Void @State var localAlias: String + @State private var showSharePicker = false @FocusState private var aliasTextFieldFocused: Bool @State private var alert: GroupChatInfoViewAlert? = nil @State private var groupLink: GroupLink? @@ -113,6 +115,11 @@ struct GroupChatInfoView: View { } label: { Label("Share link", systemImage: "square.and.arrow.up") } + Button { + showSharePicker = true + } label: { + Label("Share via chat", systemImage: "arrowshape.turn.up.forward") + } } if groupInfo.isOwner || members.contains(where: { $0.wrapped.memberRole >= .owner }) { channelMembersButton() @@ -150,19 +157,17 @@ struct GroupChatInfoView: View { if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) { addOrEditWelcomeMessage() } - if !groupInfo.useRelays { - GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) - } + GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) } footer: { - if !groupInfo.useRelays { - let label: LocalizedStringKey = ( - groupInfo.businessChat == nil - ? "Only group owners can change group preferences." - : "Only chat owners can change preferences." - ) - Text(label) - .foregroundColor(theme.colors.secondary) - } + let label: LocalizedStringKey = ( + groupInfo.useRelays + ? "Only channel owners can change channel preferences." + : groupInfo.businessChat == nil + ? "Only group owners can change group preferences." + : "Only chat owners can change preferences." + ) + Text(label) + .foregroundColor(theme.colors.secondary) } Section { @@ -248,6 +253,9 @@ struct GroupChatInfoView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .sheet(isPresented: $showSharePicker) { + shareChannelPicker(groupInfo: groupInfo, composeState: $composeState) + } .alert(item: $alert) { alertItem in switch(alertItem) { case .deleteGroupAlert: return deleteGroupAlert() @@ -638,7 +646,9 @@ struct GroupChatInfoView: View { groupLinkMemberRole: $groupLinkMemberRole, showTitle: false, creatingGroup: false, - isChannel: groupInfo.useRelays + isChannel: groupInfo.useRelays, + groupInfo: groupInfo, + composeState: $composeState ) .navigationBarTitle(groupInfo.useRelays ? "Channel link" : "Group link") .modifier(ThemedBackground(grouped: true)) @@ -976,7 +986,9 @@ struct GroupPreferencesButton: View { var creatingGroup: Bool = false private var label: LocalizedStringKey { - groupInfo.businessChat == nil ? "Group preferences" : "Chat preferences" + groupInfo.useRelays ? "Channel preferences" + : groupInfo.businessChat == nil ? "Group preferences" + : "Chat preferences" } var body: some View { @@ -993,7 +1005,9 @@ struct GroupPreferencesButton: View { .navigationBarTitleDisplayMode(.large) .onDisappear { let saveText = NSLocalizedString( - creatingGroup ? "Save" : "Save and notify group members", + creatingGroup ? "Save" + : groupInfo.useRelays ? "Save and notify subscribers" + : "Save and notify group members", comment: "alert button" ) @@ -1048,12 +1062,66 @@ func largeGroupReceiptsDisabledAlert() -> Alert { ) } +@ViewBuilder +func shareChannelPicker(groupInfo: GroupInfo, composeState: Binding? = nil) -> some View { + let v = ChatItemForwardingView( + title: "Share channel", + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelectChat: { chat in shareChatLink(chat, sourceGroupInfo: groupInfo, composeState: composeState) } + ) + if #available(iOS 16.0, *) { + v.presentationDetents([.fraction(0.8)]) + } else { + v + } +} + +func shareChatLink(_ destChat: Chat, sourceGroupInfo: GroupInfo, composeState: Binding? = nil) { + let sendAsGroup = if let gInfo = destChat.chatInfo.groupInfo { gInfo.useRelays && gInfo.membership.memberRole >= .owner } else { false } + Task { + do { + let mc = try await apiShareChatMsgContent( + shareChatType: .group, shareChatId: Int64(sourceGroupInfo.groupId), + toChatType: destChat.chatInfo.chatType, toChatId: destChat.chatInfo.apiId, + toScope: destChat.chatInfo.groupChatScope(), sendAsGroup: sendAsGroup + ) + if case let .chat(_, chatLink, ownerSig) = mc { + await MainActor.run { + dismissAllSheets { + let cs = ComposeState(preview: .chatLinkPreview(chatLink: chatLink, ownerSig: ownerSig)) + if let composeState { + composeState.wrappedValue = cs + } else { + ChatModel.shared.draft = cs + ChatModel.shared.draftChatId = destChat.id + } + if destChat.id != ChatModel.shared.chatId { + ItemsModel.shared.loadOpenChat(destChat.id) + } + } + } + } else { + logger.error("shareChatLink: unexpected MsgContent: \(String(describing: mc))") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing channel", comment: "alert title"), message: String(describing: mc)) + } + } + } catch { + logger.error("shareChatLink error: \(error.localizedDescription)") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing channel", comment: "alert title"), message: error.localizedDescription) + } + } + } +} + struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: Binding.constant(GroupInfo.sampleData), scrollToItemId: Binding.constant(nil), + composeState: Binding.constant(ComposeState()), onSearch: {}, localAlias: "" ) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 56ee370402..22253c4808 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -18,7 +18,10 @@ struct GroupLinkView: View { var showTitle: Bool = false var creatingGroup: Bool = false var isChannel: Bool = false + var groupInfo: GroupInfo? = nil + var composeState: Binding? = nil var linkCreatedCb: (() -> Void)? = nil + @State private var showSharePicker = false @State private var showShortLink = true @State private var creatingLink = false @State private var alert: GroupLinkAlert? @@ -104,6 +107,11 @@ struct GroupLinkView: View { } label: { Label("Share link", systemImage: "square.and.arrow.up") } + if groupInfo?.groupProfile.publicGroup != nil { + Button { showSharePicker = true } label: { + Label("Share via chat", systemImage: "arrowshape.turn.up.forward") + } + } if !creatingGroup && !isChannel { Button(role: .destructive) { alert = .deleteLink } label: { @@ -160,6 +168,11 @@ struct GroupLinkView: View { } } .modifier(ThemedBackground(grouped: true)) + .sheet(isPresented: $showSharePicker) { + if let gInfo = groupInfo { + shareChannelPicker(groupInfo: gInfo, composeState: composeState) + } + } } private func createGroupLink() { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 55b1dc6d2e..84e852f5a3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -27,26 +27,33 @@ struct GroupPreferencesView: View { @State private var showSaveDialogue = false var body: some View { - let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members" + let saveText: LocalizedStringKey = creatingGroup ? "Save" : groupInfo.useRelays ? "Save and notify subscribers" : "Save and notify group members" VStack { List { - Section { - MemberAdmissionButton( - groupInfo: $groupInfo, - admission: groupInfo.groupProfile.memberAdmission_, - currentAdmission: groupInfo.groupProfile.memberAdmission_, - creatingGroup: creatingGroup - ) + if !groupInfo.useRelays { + Section { + MemberAdmissionButton( + groupInfo: $groupInfo, + admission: groupInfo.groupProfile.memberAdmission_, + currentAdmission: groupInfo.groupProfile.memberAdmission_, + creatingGroup: creatingGroup + ) + } + featureSection(.timedMessages, $preferences.timedMessages.enable) + featureSection(.fullDelete, $preferences.fullDelete.enable) + featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) + featureSection(.reactions, $preferences.reactions.enable) + featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) + featureSection(.files, $preferences.files.enable, $preferences.files.role) + featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) + featureSection(.reports, $preferences.reports.enable) + featureSection(.history, $preferences.history.enable) + } else { + featureSection(.timedMessages, $preferences.timedMessages.enable) + featureSection(.fullDelete, $preferences.fullDelete.enable) + featureSection(.reactions, $preferences.reactions.enable) + featureSection(.history, $preferences.history.enable) } - featureSection(.timedMessages, $preferences.timedMessages.enable) - featureSection(.fullDelete, $preferences.fullDelete.enable) - featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) - featureSection(.reactions, $preferences.reactions.enable) - featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) - featureSection(.files, $preferences.files.enable, $preferences.files.role) - featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) - featureSection(.reports, $preferences.reports.enable) - featureSection(.history, $preferences.history.enable) if groupInfo.isOwner { Section { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 24a52b4b60..126fcc57b3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -37,7 +37,7 @@ struct GroupProfileView: View { var body: some View { List { - EditProfileImage(profileImage: $groupProfile.image, showChooseSource: $showChooseSource) + EditProfileImage(profileImage: $groupProfile.image, iconName: groupInfo.chatIconName, showChooseSource: $showChooseSource) .if(!focusDisplayName) { $0.padding(.top) } Section { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 3050b0d4cd..967fedf293 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -38,8 +38,9 @@ enum PresetTag: Int, Identifiable, CaseIterable, Equatable { case favorites = 1 case contacts = 2 case groups = 3 - case business = 4 - case notes = 5 + case channels = 4 + case business = 5 + case notes = 6 var id: Int { rawValue } @@ -293,36 +294,40 @@ struct ChatListView: View { @ToolbarContentBuilder var topToolbar: some ToolbarContent { ToolbarItem(placement: .topBarLeading) { leadingToolbarItem } - ToolbarItem(placement: .principal) { SubsStatusIndicator() } + ToolbarItem(placement: .principal) { if !shouldShowOnboarding { SubsStatusIndicator() } } ToolbarItem(placement: .topBarTrailing) { trailingToolbarItem } } - + @ToolbarContentBuilder var bottomToolbar: some ToolbarContent { let padding: Double = Self.hasHomeIndicator ? 0 : 14 ToolbarItem(placement: .bottomBar) { HStack { leadingToolbarItem.padding(.bottom, padding) Spacer() - SubsStatusIndicator().padding(.bottom, padding) - Spacer() + if !shouldShowOnboarding { + SubsStatusIndicator().padding(.bottom, padding) + Spacer() + } trailingToolbarItem.padding(.bottom, padding) } .contentShape(Rectangle()) .onTapGesture { scrollToSearchBar = true } } } - + @ToolbarContentBuilder func bottomToolbarGroup() -> some ToolbarContent { let padding: Double = Self.hasHomeIndicator ? 0 : 14 ToolbarItemGroup(placement: viewOnScreen ? .bottomBar : .principal) { leadingToolbarItem.padding(.bottom, padding) Spacer() - SubsStatusIndicator().padding(.bottom, padding) - Spacer() + if !shouldShowOnboarding { + SubsStatusIndicator().padding(.bottom, padding) + Spacer() + } trailingToolbarItem.padding(.bottom, padding) } } - + @ViewBuilder var leadingToolbarItem: some View { let user = chatModel.currentUser ?? User.sampleData ZStack(alignment: .topTrailing) { @@ -348,7 +353,34 @@ struct ChatListView: View { } } - private var chatList: some View { + private var shouldShowOnboarding: Bool { + !addressCreationCardShown && !chatModel.chats.isEmpty && !hasConversations + } + + private var hasConversations: Bool { + chatModel.chats.contains { chat in + switch chat.chatInfo { + case .local: return false + case let .direct(contact): return !contact.chatDeleted && !contact.isContactCard + case .group: return true + case .contactRequest: return false + case .contactConnection: return false + case .invalidJSON: return false + } + } + } + + @ViewBuilder private var chatList: some View { + if shouldShowOnboarding { + ConnectOnboardingView() + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .modifier(ThemedBackground()) + } else { + chatListContent + } + } + + private var chatListContent: some View { let cs = filteredChats() return ZStack { ScrollViewReader { scrollProxy in @@ -395,8 +427,8 @@ struct ChatListView: View { .listRowSeparator(.hidden) .listRowBackground(Color.clear) } - if !addressCreationCardShown { - AddressCreationCard() + if !addressCreationCardShown && hasConversations { + ConnectBannerCard() .padding(.vertical, 6) .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .listRowSeparator(.hidden) @@ -881,6 +913,7 @@ struct TagsView: View { case .favorites: (active ? "star.fill" : "star", "Favorites") case .contacts: (active ? "person.fill" : "person", "Contacts") case .groups: (active ? "person.2.fill" : "person.2", "Groups") + case .channels: (active ? "antenna.radiowaves.left.and.right.circle.fill" : "antenna.radiowaves.left.and.right.circle", "Channels") case .business: (active ? "briefcase.fill" : "briefcase", "Businesses") case .notes: (active ? "folder.fill" : "folder", "Notes") } @@ -924,7 +957,12 @@ func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: C } case .groups: switch chatInfo { - case let .group(groupInfo, _): groupInfo.businessChat == nil + case let .group(groupInfo, _): groupInfo.businessChat == nil && !groupInfo.isChannel + default: false + } + case .channels: + switch chatInfo { + case let .group(groupInfo, _): groupInfo.isChannel default: false } case .business: diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 3524ceff18..243d804685 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -296,11 +296,23 @@ struct ChatPreviewView: View { } func chatItemPreview(_ cItem: ChatItem) -> (Text, Bool) { - let itemText = cItem.meta.itemDeleted == nil ? cItem.text(isChannel: chat.chatInfo.isChannel) : markedDeletedText() - let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil + let (itemText, itemFormattedText) = chatItemPreviewText(cItem) let r = messageText(itemText, itemFormattedText, sender: cItem.meta.showGroupAsSender ? nil : cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix()) return (Text(AttributedString(r.string)), r.hasSecrets) + func chatItemPreviewText(_ ci: ChatItem) -> (String, [FormattedText]?) { + if ci.meta.itemDeleted != nil { + return (markedDeletedText(), nil) + } + if case let .chat(_, chatLink, _) = ci.content.msgContent { + let descr = if let descr = chatLink.shortDescription?.trimmingCharacters(in: .whitespacesAndNewlines), + descr != "" { "\n" + descr } else { "" } + let text = chatLink.displayName + descr + return (text, nil) + } + return (ci.text(isChannel: chat.chatInfo.isChannel), ci.formattedText) + } + // same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey; // can be refactored into a single function if functions calling these are changed to return same type func markedDeletedText() -> String { @@ -426,6 +438,18 @@ struct ChatPreviewView: View { smallContentPreviewFile(size: dynamicMediaSize) { CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallViewSize: dynamicMediaSize) } + case let .chat(_, chatLink, ownerSig): + smallContentPreview(size: dynamicMediaSize, borderColor: chatLink.image != nil ? .secondary : .clear) { + ProfileImage( + imageStr: chatLink.image, + iconName: chatLink.iconName, + size: dynamicMediaSize, + color: Color(uiColor: .tertiaryLabel) + ) + .onTapGesture { + planAndConnect(chatLink.connLinkStr, linkOwnerSig: ownerSig, theme: theme, dismiss: false) + } + } default: EmptyView() } } @@ -499,12 +523,12 @@ func flagIcon(size: CGFloat, color: Color) -> some View { .foregroundColor(color) } -func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View { +func smallContentPreview(size: CGFloat, borderColor: Color = .secondary, _ view: @escaping () -> some View) -> some View { view() .frame(width: size, height: size) .cornerRadius(8) .overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) + .strokeBorder(borderColor, lineWidth: 0.3, antialiased: true)) .padding(.vertical, size / 6) .padding(.leading, 3) .offset(x: 6) diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 2ef928c7c5..8b982ec0b7 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -101,25 +101,28 @@ class OpenChatAlertViewController: UIViewController { private let profileFullName: String private let profileImage: UIView private let subtitle: String? + private let information: String? private let cancelTitle: String - private let confirmTitle: String + private let confirmTitle: String? private let onCancel: () -> Void - private let onConfirm: () -> Void + private let onConfirm: (() -> Void)? init( profileName: String, profileFullName: String, profileImage: UIView, subtitle: String? = nil, + information: String? = nil, cancelTitle: String = "Cancel", - confirmTitle: String = "Open", - onCancel: @escaping () -> Void, - onConfirm: @escaping () -> Void + confirmTitle: String? = "Open", + onCancel: @escaping () -> Void = {}, + onConfirm: (() -> Void)? = nil ) { self.profileName = profileName self.profileFullName = profileFullName self.profileImage = profileImage self.subtitle = subtitle + self.information = information self.cancelTitle = cancelTitle self.confirmTitle = confirmTitle self.onCancel = onCancel @@ -180,12 +183,24 @@ class OpenChatAlertViewController: UIViewController { subtitleLabel.text = subtitle subtitleLabel.font = UIFont.preferredFont(forTextStyle: .footnote) subtitleLabel.textColor = .secondaryLabel - subtitleLabel.numberOfLines = 1 + subtitleLabel.numberOfLines = 3 subtitleLabel.textAlignment = .center subtitleLabel.translatesAutoresizingMaskIntoConstraints = false profileViews.append(subtitleLabel) } + // Information label (e.g. owner verification) + if let information { + let infoLabel = UILabel() + infoLabel.text = information + infoLabel.font = UIFont.preferredFont(forTextStyle: .footnote) + infoLabel.textColor = .label + infoLabel.numberOfLines = 3 + infoLabel.textAlignment = .center + infoLabel.translatesAutoresizingMaskIntoConstraints = false + profileViews.append(infoLabel) + } + // Horizontal stack for image + name let stack = UIStackView(arrangedSubviews: profileViews) stack.axis = .vertical @@ -211,20 +226,54 @@ class OpenChatAlertViewController: UIViewController { cancelButton.titleLabel?.font = UIFont(descriptor: bodyDescr.withSymbolicTraits(.traitBold) ?? bodyDescr, size: 0) cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside) - let confirmButton = UIButton(type: .system) - confirmButton.setTitle(confirmTitle, for: .normal) - confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) - confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + let buttonStack: UIStackView + var buttonDividerConstraints: [NSLayoutConstraint] = [] - let verticalButtons = cancelButton.intrinsicContentSize.width + 20 >= alertWidth / 2 || confirmButton.intrinsicContentSize.width + 20 >= alertWidth / 2 + if let confirmTitle { + let confirmButton = UIButton(type: .system) + confirmButton.setTitle(confirmTitle, for: .normal) + confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) - // Button stack with equal width buttons - let buttonStack = UIStackView(arrangedSubviews: verticalButtons ? [confirmButton, cancelButton] : [cancelButton, confirmButton]) - buttonStack.axis = verticalButtons ? .vertical : .horizontal - buttonStack.distribution = .fillEqually - buttonStack.spacing = 0 // no spacing, use divider instead - buttonStack.translatesAutoresizingMaskIntoConstraints = false - buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * (verticalButtons ? 2 : 1)).isActive = true + let verticalButtons = cancelButton.intrinsicContentSize.width + 20 >= alertWidth / 2 || confirmButton.intrinsicContentSize.width + 20 >= alertWidth / 2 + + // Button stack with equal width buttons + buttonStack = UIStackView(arrangedSubviews: verticalButtons ? [confirmButton, cancelButton] : [cancelButton, confirmButton]) + buttonStack.axis = verticalButtons ? .vertical : .horizontal + buttonStack.distribution = .fillEqually + buttonStack.spacing = 0 // no spacing, use divider instead + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * (verticalButtons ? 2 : 1)).isActive = true + + // Add divider between buttons + let buttonDivider = UIView() + buttonDivider.backgroundColor = UIColor.separator + buttonDivider.translatesAutoresizingMaskIntoConstraints = false + buttonStack.addSubview(buttonDivider) + + buttonDividerConstraints = if verticalButtons { + [ + buttonDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + buttonDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + buttonDivider.centerYAnchor.constraint(equalTo: buttonStack.centerYAnchor), + buttonDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } else { + [ + buttonDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor), + buttonDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + buttonDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor), + buttonDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } + } else { + // Single button + buttonStack = UIStackView(arrangedSubviews: [cancelButton]) + buttonStack.axis = .horizontal + buttonStack.distribution = .fillEqually + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight).isActive = true + } // Vertical stack containing hStack and buttonStack let vStack = UIStackView(arrangedSubviews: [topRowContainer, buttonStack]) @@ -241,29 +290,6 @@ class OpenChatAlertViewController: UIViewController { horizontalDivider.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(horizontalDivider) - // Add divider between buttons - let buttonDivider = UIView() - buttonDivider.backgroundColor = UIColor.separator - buttonDivider.translatesAutoresizingMaskIntoConstraints = false - buttonStack.addSubview(buttonDivider) - - // Constraints - let buttonDividerConstraints = if verticalButtons { - [ - buttonDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - buttonDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - buttonDivider.centerYAnchor.constraint(equalTo: buttonStack.centerYAnchor), - buttonDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) - ] - } else { - [ - buttonDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor), - buttonDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - buttonDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor), - buttonDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) - ] - } - NSLayoutConstraint.activate([ // Container view centering and fixed width containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), @@ -295,7 +321,7 @@ class OpenChatAlertViewController: UIViewController { @objc private func confirmTapped() { dismiss(animated: true) { - self.onConfirm() + self.onConfirm?() } } } @@ -307,10 +333,11 @@ func showOpenChatAlert( profileImage: Content, theme: AppTheme, subtitle: String? = nil, + information: String? = nil, cancelTitle: String = "Cancel", - confirmTitle: String = "Open", + confirmTitle: String? = "Open", onCancel: @escaping () -> Void = {}, - onConfirm: @escaping () -> Void + onConfirm: (() -> Void)? = nil ) { let themedView = profileImage.environmentObject(theme) let hostingController = UIHostingController(rootView: themedView) @@ -323,6 +350,7 @@ func showOpenChatAlert( profileFullName: profileFullName, profileImage: hostedView, subtitle: subtitle, + information: information, cancelTitle: cancelTitle, confirmTitle: confirmTitle, onCancel: onCancel, diff --git a/apps/ios/Shared/Views/NewChat/AddChannelView.swift b/apps/ios/Shared/Views/NewChat/AddChannelView.swift index 098cccef1b..4e9a42971c 100644 --- a/apps/ios/Shared/Views/NewChat/AddChannelView.swift +++ b/apps/ios/Shared/Views/NewChat/AddChannelView.swift @@ -47,7 +47,7 @@ struct AddChannelView: View { Group { ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, size: 128) + ProfileImage(imageStr: profile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128) if profile.image != nil { Button { profile.image = nil @@ -174,20 +174,32 @@ struct AddChannelView: View { } return } - guard let (gInfo, gLink, gRelays) = try await apiNewPublicGroup( + guard let result = try await apiNewPublicGroup( incognito: false, relayIds: relayIds, groupProfile: profile ) else { await MainActor.run { creationInProgress = false } return } - await MainActor.run { - m.updateGroup(gInfo) - m.creatingChannelId = gInfo.id - groupInfo = gInfo - groupLink = gLink - groupRelays = gRelays.sorted { relayDisplayName($0) < relayDisplayName($1) } - channelRelaysModel.set(groupId: gInfo.groupId, groupRelays: gRelays) - creationInProgress = false + switch result { + case let .created(gInfo, gLink, gRelays): + await MainActor.run { + m.updateGroup(gInfo) + m.creatingChannelId = gInfo.id + groupInfo = gInfo + groupLink = gLink + groupRelays = gRelays.sorted { relayDisplayName($0) < relayDisplayName($1) } + channelRelaysModel.set(groupId: gInfo.groupId, groupRelays: gRelays) + creationInProgress = false + } + case let .creationFailed(relayResults): + await MainActor.run { + creationInProgress = false + showAlert( + NSLocalizedString("Error creating channel", comment: "alert title"), + message: NSLocalizedString("Relay results:", comment: "alert message") + "\n" + + relayResults.map { "\(chatRelayDisplayName($0.relay)): \($0.relayError.map { connErrorText($0) } ?? "ok")" }.joined(separator: "\n") + ) + } } } catch { await MainActor.run { @@ -210,6 +222,7 @@ struct AddChannelView: View { var operatorGroups: [[UserChatRelay]] = [] var customRelays: [UserChatRelay] = [] for op in servers { + guard op.operator?.enabled ?? true else { continue } let relays = op.chatRelays.filter { $0.enabled && !$0.deleted && $0.chatRelayId != nil } guard !relays.isEmpty else { continue } if op.operator != nil { @@ -244,6 +257,7 @@ struct AddChannelView: View { private func checkHasRelays() async -> Bool { guard let servers = try? await getUserServers() else { return false } return servers.contains { op in + (op.operator?.enabled ?? true) && op.chatRelays.contains { $0.enabled && !$0.deleted && $0.chatRelayId != nil } } } @@ -256,7 +270,7 @@ struct AddChannelView: View { let total = groupRelays.count return List { Group { - ProfileImage(imageStr: gInfo.groupProfile.image, size: 128) + ProfileImage(imageStr: gInfo.groupProfile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128) .frame(maxWidth: .infinity, alignment: .center) Text(gInfo.groupProfile.displayName) @@ -373,7 +387,8 @@ struct AddChannelView: View { groupLinkMemberRole: Binding.constant(.observer), // TODO [relays] starting role should be communicated in protocol from owner to relays showTitle: false, creatingGroup: true, - isChannel: true + isChannel: true, + groupInfo: gInfo ) { m.creatingChannelId = nil DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -428,9 +443,15 @@ func relayDisplayName(_ relay: GroupRelay) -> String { return "relay \(relay.groupRelayId)" } -func relayStatusIndicator(_ status: RelayStatus, connFailed: Bool = false) -> some View { - let color: Color = connFailed ? .red : (status == .rsActive ? .green : .yellow) - let text: LocalizedStringKey = connFailed ? "failed" : status.text +private func chatRelayDisplayName(_ relay: UserChatRelay) -> String { + if !relay.displayName.isEmpty { return relay.displayName } + return relay.address +} + +func relayStatusIndicator(_ status: RelayStatus, connFailed: Bool = false, memberStatus: GroupMemberStatus? = nil) -> some View { + let removed = memberStatus.map { [.memLeft, .memRemoved, .memGroupDeleted].contains($0) } ?? false + let color: Color = connFailed || removed ? .red : (status == .rsActive ? .green : .yellow) + let text: LocalizedStringKey = connFailed ? "failed" : memberStatus == .memLeft ? "removed by operator" : status.text return HStack(spacing: 4) { Circle() .fill(color) diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index c74e016974..3ce4d1fa40 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct AddGroupView: View { + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @@ -66,29 +67,39 @@ struct AddGroupView: View { func createGroupView() -> some View { List { Group { - ZStack(alignment: .center) { - ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, size: 128) - if profile.image != nil { - Button { - profile.image = nil - } label: { - Image(systemName: "multiply") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12) + HStack(spacing: 0) { + ZStack(alignment: .center) { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: profile.image, iconName: "person.2.circle.fill", size: 128) + if profile.image != nil { + Button { + profile.image = nil + } label: { + Image(systemName: "multiply") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12) + } } } - } - editImageButton { showChooseSource = true } - .buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable + editImageButton { showChooseSource = true } + .buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable + } + .frame(maxWidth: .infinity) + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? "create-group" : "create-group-light") + .resizable() + .scaledToFit() + .frame(height: 140) + .frame(maxWidth: .infinity) + #endif } - .frame(maxWidth: .infinity, alignment: .center) + .frame(maxWidth: .infinity) } .listRowBackground(Color.clear) .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)) + .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)) Section { groupNameTextField() diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 8e62923f3f..177f8761f4 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -55,7 +55,7 @@ struct NewChatSheet: View { let showArchive = chatModel.chats.contains { $0.chatInfo.contact?.chatDeleted == true } let v = NavigationView { viewBody(showArchive) - .navigationTitle("New message") + .navigationTitle("New chat") .navigationBarTitleDisplayMode(.large) .navigationBarHidden(searchMode) .modifier(ThemedBackground(grouped: true)) @@ -99,9 +99,8 @@ struct NewChatSheet: View { Section { NavigationLink(isActive: $isAddContactActive) { NewChatView(selection: .invite) - .navigationTitle("New chat") .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) } label: { navigateOnTap(Label("Create 1-time link", systemImage: "link.badge.plus")) { isAddContactActive = true @@ -109,9 +108,8 @@ struct NewChatSheet: View { } NavigationLink(isActive: $isScanPasteLinkActive) { NewChatView(selection: .connect, showQRCodeScanner: true) - .navigationTitle("New chat") .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) } label: { navigateOnTap(Label("Scan / Paste link", systemImage: "qrcode")) { isScanPasteLinkActive = true @@ -131,7 +129,7 @@ struct NewChatSheet: View { .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - Label("Create public channel (BETA)", systemImage: "antenna.radiowaves.left.and.right.circle.fill") + Label("Create public channel (BETA)", systemImage: "antenna.radiowaves.left.and.right") } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 63fb7f5221..fab8f8a143 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -80,6 +80,7 @@ struct NewChatView: View { @EnvironmentObject var theme: AppTheme @State var selection: NewChatOption @State var showQRCodeScanner = false + var onboarding: Bool = false @State private var invitationUsed: Bool = false @State private var connLinkInvitation: CreatedConnLink = CreatedConnLink(connFullLink: "", connShortLink: nil) @State private var showShortLink = true @@ -91,17 +92,19 @@ struct NewChatView: View { var body: some View { VStack(alignment: .leading) { - Picker("New chat", selection: $selection) { - Label("1-time link", systemImage: "link") - .tag(NewChatOption.invite) - Label("Connect via link", systemImage: "qrcode") - .tag(NewChatOption.connect) - } - .pickerStyle(.segmented) - .padding() - .onChange(of: $selection.wrappedValue) { opt in - if opt == NewChatOption.connect { - showQRCodeScanner = true + if !onboarding { + Picker("New chat", selection: $selection) { + Label("1-time link", systemImage: "link") + .tag(NewChatOption.invite) + Label("Connect via link", systemImage: "qrcode") + .tag(NewChatOption.connect) + } + .pickerStyle(.segmented) + .padding() + .onChange(of: $selection.wrappedValue) { opt in + if opt == NewChatOption.connect { + showQRCodeScanner = true + } } } @@ -116,7 +119,7 @@ struct NewChatView: View { } } if case .connect = selection { - ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert) + ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert, onboarding: onboarding) .transition(.move(edge: .trailing)) } } @@ -141,16 +144,22 @@ struct NewChatView: View { } default: () } - } + }, + including: onboarding ? .subviews : .all ) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - InfoSheetButton { - AddContactLearnMore(showTitle: true) + if !onboarding { + InfoSheetButton { + AddContactLearnMore(showTitle: true) + } + } else { + Image(systemName: "info.circle").opacity(0) } } } + .if(onboarding) { $0.navigationBarTitleDisplayMode(.inline) } .modifier(ThemedBackground(grouped: true)) .onChange(of: invitationUsed) { used in if used && !(m.showingInvitation?.connChatUsed ?? true) { @@ -179,7 +188,8 @@ struct NewChatView: View { contactConnection: $contactConnection, connLinkInvitation: $connLinkInvitation, showShortLink: $showShortLink, - choosingProfile: $choosingProfile + choosingProfile: $choosingProfile, + onboarding: onboarding ) } else if creatingConnReq { creatingLinkProgressView() @@ -239,6 +249,7 @@ private func incognitoProfileImage() -> some View { } private struct InviteView: View { + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @Binding var invitationUsed: Bool @@ -246,18 +257,19 @@ private struct InviteView: View { @Binding var connLinkInvitation: CreatedConnLink @Binding var showShortLink: Bool @Binding var choosingProfile: Bool + var onboarding: Bool = false @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false var body: some View { List { - Section(header: Text("Share this 1-time invite link").foregroundColor(theme.colors.secondary)) { + Section(header: sectionHeader) { shareLinkView() } .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) qrCodeView() - if let selectedProfile = chatModel.currentUser { + if !onboarding, let selectedProfile = chatModel.currentUser { Section { NavigationLink { ActiveProfilePicker( @@ -281,9 +293,9 @@ private struct InviteView: View { } header: { Text("Share profile").foregroundColor(theme.colors.secondary) } footer: { - if incognitoDefault { - Text("A new random profile will be shared.") - } + if incognitoDefault { + Text("A new random profile will be shared.") + } } } } @@ -295,16 +307,52 @@ private struct InviteView: View { } } + private var sectionHeader: some View { + #if SIMPLEX_ASSETS + VStack(alignment: .leading, spacing: 0) { + Image(colorScheme == .light + ? (onboarding ? "one-time-link" : "one-time-link-small") + : (onboarding ? "one-time-link-light" : "one-time-link-small-light")) + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + sectionHeaderText + } + .padding(.bottom, 6) + #else + sectionHeaderText + .if(onboarding) { $0.padding(.bottom, 6) } + #endif + } + + @ViewBuilder private var sectionHeaderText: some View { + if onboarding { + Text("Send the link via any messenger - it's secure. Ask to paste into SimpleX.") + .font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } else { + Text("Share this 1-time invite link").foregroundColor(theme.colors.secondary) + } + } + private func shareLinkView() -> some View { - HStack { + HStack(spacing: 8) { let link = connLinkInvitation.simplexChatUri(short: showShortLink) linkTextView(link) + Button { + UIPasteboard.general.string = link + setInvitationUsed() + } label: { + Image(systemName: "doc.on.doc") + .padding(.top, -7) + .padding(.horizontal, 8) + } Button { showShareSheet(items: [link]) setInvitationUsed() } label: { Image(systemName: "square.and.arrow.up") .padding(.top, -7) + .padding(.horizontal, 8) } } .frame(maxWidth: .infinity) @@ -324,7 +372,11 @@ private struct InviteView: View { .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } header: { - ToggleShortLinkHeader(text: Text("Or show this code"), link: connLinkInvitation, short: $showShortLink) + if onboarding { + Text("Or show QR in person or via video call.").font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } else { + ToggleShortLinkHeader(text: Text("Or show this code"), link: connLinkInvitation, short: $showShortLink) + } } } @@ -587,20 +639,24 @@ private struct ActiveProfilePicker: View { } private struct ConnectView: View { + @Environment(\.colorScheme) var colorScheme @StateObject private var connectProgressManager = ConnectProgressManager.shared @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var theme: AppTheme @Binding var showQRCodeScanner: Bool @Binding var pastedLink: String @Binding var alert: NewChatViewAlert? + var onboarding: Bool = false @State var scannerPaused: Bool = false @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings var body: some View { List { - Section(header: Text("Paste the link you received").foregroundColor(theme.colors.secondary)) { + Section(header: connectSectionHeader) { pasteLinkView() } + .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) { ScannerInView(showQRCodeScanner: $showQRCodeScanner, scannerPaused: $scannerPaused, processQRCode: processQRCode) } @@ -630,7 +686,7 @@ private struct ConnectView: View { } } } label: { - Text("Tap to paste link") + Text("Tap to paste link").foregroundColor(theme.colors.primary) } .disabled(!pasteboardHasStrings) .frame(maxWidth: .infinity, alignment: .center) @@ -669,6 +725,23 @@ private struct ConnectView: View { } } + private var connectSectionHeader: some View { + #if SIMPLEX_ASSETS + VStack(alignment: .leading, spacing: 0) { + Image(colorScheme == .light + ? (onboarding ? "connect-via-link" : "connect-via-link-small") + : (onboarding ? "connect-via-link-light" : "connect-via-link-small-light")) + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + Text("Paste the link you received").foregroundColor(theme.colors.secondary) + } + .padding(.bottom, 4) + #else + Text("Paste the link you received").foregroundColor(theme.colors.secondary) + #endif + } + private func connect(_ link: String) { scannerPaused = true planAndConnect( @@ -765,7 +838,7 @@ struct ScannerInView: View { } -private func linkTextView(_ link: String) -> some View { +func linkTextView(_ link: String) -> some View { Text(link) .lineLimit(1) .font(.caption) @@ -916,11 +989,13 @@ private func showAskCurrentOrIncognitoProfileSheet( actionStyle: UIAlertAction.Style = .default, connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, + ownerVerification: OwnerVerification? = nil, dismiss: Bool, cleanup: (() -> Void)? ) { showSheet( title, + message: ownerVerificationMessage(ownerVerification), actions: {[ UIAlertAction( title: NSLocalizedString("Use current profile", comment: "new chat action"), @@ -1056,6 +1131,7 @@ private func showOwnGroupLinkConfirmConnectSheet( private func showPrepareContactAlert( connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, + ownerVerification: OwnerVerification? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1074,6 +1150,7 @@ private func showPrepareContactAlert( size: alertProfileImageSize ), theme: theme, + information: ownerVerificationMessage(ownerVerification), cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), confirmTitle: NSLocalizedString("Open new chat", comment: "new chat action"), onCancel: { cleanup?() }, @@ -1101,6 +1178,7 @@ private func showPrepareGroupAlert( connectionLink: CreatedConnLink, groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, + ownerVerification: OwnerVerification? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1120,6 +1198,7 @@ private func showPrepareGroupAlert( ), theme: theme, subtitle: isChannel ? subscriberCount : nil, + information: ownerVerificationMessage(ownerVerification), cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), confirmTitle: isChannel ? NSLocalizedString("Open new channel", comment: "new chat action") @@ -1217,6 +1296,7 @@ private func showOpenKnownGroupAlert( // Spec: spec/client/navigation.md#planAndConnect func planAndConnect( _ shortOrFullLink: String, + linkOwnerSig: LinkOwnerSig? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? = nil, @@ -1241,7 +1321,7 @@ func planAndConnect( func connectTask(_ inProgress: BoxedValue) { Task { - let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, inProgress: inProgress) + let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) await MainActor.run { ConnectProgressManager.shared.stopConnectProgress() } @@ -1250,13 +1330,14 @@ func planAndConnect( switch connectionPlan { case let .invitationLink(ilp): switch ilp { - case let .ok(contactSLinkData_): + case let .ok(contactSLinkData_, ownerVerification): if let contactSLinkData = contactSLinkData_ { logger.debug("planAndConnect, .invitationLink, .ok, short link data present") await MainActor.run { showPrepareContactAlert( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, + ownerVerification: ownerVerification, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1269,6 +1350,7 @@ func planAndConnect( title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"), connectionLink: connectionLink, connectionPlan: connectionPlan, + ownerVerification: ownerVerification, dismiss: dismiss, cleanup: cleanup ) @@ -1311,13 +1393,14 @@ func planAndConnect( } case let .contactAddress(cap): switch cap { - case let .ok(contactSLinkData_): + case let .ok(contactSLinkData_, ownerVerification): if let contactSLinkData = contactSLinkData_ { logger.debug("planAndConnect, .contactAddress, .ok, short link data present") await MainActor.run { showPrepareContactAlert( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, + ownerVerification: ownerVerification, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1330,6 +1413,7 @@ func planAndConnect( title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"), connectionLink: connectionLink, connectionPlan: connectionPlan, + ownerVerification: ownerVerification, dismiss: dismiss, cleanup: cleanup ) @@ -1389,7 +1473,7 @@ func planAndConnect( } case let .groupLink(glp): switch glp { - case let .ok(groupShortLinkInfo_, groupSLinkData_): + case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification): if let groupSLinkData = groupSLinkData_ { logger.debug("planAndConnect, .groupLink, .ok, short link data present") await MainActor.run { @@ -1397,6 +1481,7 @@ func planAndConnect( connectionLink: connectionLink, groupShortLinkInfo: groupShortLinkInfo_, groupShortLinkData: groupSLinkData, + ownerVerification: ownerVerification, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1409,6 +1494,7 @@ func planAndConnect( title: NSLocalizedString("Join group", comment: "new chat sheet title"), connectionLink: connectionLink, connectionPlan: connectionPlan, + ownerVerification: ownerVerification, dismiss: dismiss, cleanup: cleanup ) @@ -1454,6 +1540,33 @@ func planAndConnect( showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss) } } + case let .noRelays(groupSLinkData_): + logger.debug("planAndConnect, .groupLink, .noRelays") + await MainActor.run { + if let groupSLinkData = groupSLinkData_ { + showOpenChatAlert( + profileName: groupSLinkData.groupProfile.displayName, + profileFullName: groupSLinkData.groupProfile.fullName, + profileImage: + ProfileImage( + imageStr: groupSLinkData.groupProfile.image, + iconName: "antenna.radiowaves.left.and.right.circle.fill", + size: alertProfileImageSize + ), + theme: theme, + subtitle: NSLocalizedString("Channel has no active relays. Please try to join later.", comment: "alert subtitle"), + cancelTitle: NSLocalizedString("OK", comment: "alert button"), + confirmTitle: nil, + onCancel: { cleanup?() } + ) + } else { + showAlert( + NSLocalizedString("Channel temporarily unavailable", comment: "alert title"), + message: NSLocalizedString("Channel has no active relays. Please try to join later.", comment: "alert message") + ) + cleanup?() + } + } } case let .error(chatError): logger.debug("planAndConnect, .error \(chatErrorString(chatError))") @@ -1602,6 +1715,14 @@ private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType? } } +private func ownerVerificationMessage(_ ov: OwnerVerification?) -> String? { + switch ov { + case .verified: NSLocalizedString("Link signature verified.", comment: "owner verification") + case let .failed(reason): String.localizedStringWithFormat(NSLocalizedString("⚠️ Signature verification failed: %@.", comment: "owner verification"), reason) + case .none: nil + } +} + func connReqSentAlert(_ type: ConnReqType) -> Alert { return mkAlert( title: "Connection request sent!", diff --git a/apps/ios/Shared/Views/NewChat/OnboardingCards.swift b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift new file mode 100644 index 0000000000..913fdf5577 --- /dev/null +++ b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift @@ -0,0 +1,309 @@ +// +// OnboardingCards.swift +// SimpleX (iOS) +// +// Created by simplex-chat on 06.04.2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// MARK: - Card component + +struct OnboardingCardView: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + let imageName: String + let icon: String + let title: LocalizedStringKey + var subtitle: LocalizedStringKey? = nil + let labelHeightRatio: CGFloat + let action: () -> Void + + static let lightStops: [Gradient.Stop] = [ + .init(color: Color(red: 0.824, green: 0.910, blue: 1.0), location: 0.0), + .init(color: Color(red: 0.800, green: 0.914, blue: 1.0), location: 0.5), + .init(color: Color(red: 0.875, green: 1.0, blue: 1.0), location: 0.9), + .init(color: Color(red: 1.0, green: 0.988, blue: 0.918), location: 1.0) + ] + + static let darkStops: [Gradient.Stop] = [ + .init(color: Color(red: 0.016, green: 0.039, blue: 0.141), location: 0.4), + .init(color: Color(red: 0.220, green: 0.329, blue: 0.671), location: 0.72), + .init(color: Color(red: 0.659, green: 0.929, blue: 0.953), location: 0.9), + .init(color: Color(red: 1.0, green: 0.965, blue: 0.878), location: 1.0) + ] + + static let gradientAngle: Double = 80.0 * .pi / 180.0 + + static func gradientPoints(aspectRatio: CGFloat, scale: CGFloat) -> (start: UnitPoint, end: UnitPoint) { + let r = Double(aspectRatio) + let s = Double(scale) + let dx = cos(gradientAngle) + let dy = -sin(gradientAngle) / r + let dLenSq = dx * dx + dy * dy + let projections = [ + -0.5 * dx + (-0.5) * dy, + 0.5 * dx + (-0.5) * dy, + -0.5 * dx + 0.5 * dy, + 0.5 * dx + 0.5 * dy + ] + let tMin = projections.min()! + let tMax = projections.max()! + let startX = 0.5 + tMin * dx / dLenSq + let startY = 0.5 + tMin * dy / dLenSq + let endX = 0.5 + tMax * dx / dLenSq + let endY = 0.5 + tMax * dy / dLenSq + return ( + start: .init(x: 0.5 + (startX - 0.5) * s, y: 0.5 + (startY - 0.5) * s), + end: .init(x: 0.5 + (endX - 0.5) * s, y: 0.5 + (endY - 0.5) * s) + ) + } + + var body: some View { + Button(action: action) { + GeometryReader { geo in + let labelHeight = geo.size.width * labelHeightRatio + let imageHeight = max(geo.size.height - labelHeight, 1) + let imageAspect = imageHeight / geo.size.width + let gp = Self.gradientPoints(aspectRatio: imageAspect, scale: colorScheme == .light ? 1.2 : 1.5) + VStack(spacing: 0) { + ZStack { + LinearGradient( + stops: colorScheme == .light ? Self.lightStops : Self.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? imageName : "\(imageName)-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() + #else + Image(systemName: icon) + .font(.system(size: imageHeight * 0.25)) + .foregroundColor(theme.colors.primary) + #endif + } + .frame(height: imageHeight) + + labelRow(height: labelHeight) + } + } + .clipShape(RoundedRectangle(cornerRadius: 24)) + } + .buttonStyle(.plain) + } + + private func labelRow(height: CGFloat) -> some View { + VStack { + HStack { + #if SIMPLEX_ASSETS + Image(systemName: icon) + .font(.system(size: 24)) + .foregroundColor(theme.colors.primary) + #endif + Text(title) + .font(.body) + .fontWeight(.medium) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + .minimumScaleFactor(0.75) + } + if let subtitle { + Text(subtitle) + .font(.footnote) + .foregroundColor(theme.colors.onBackground.opacity(0.7)) + } + } + .frame(height: height) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 16) + .background(ToolbarMaterial.material(toolbarMaterial)) + } +} + +// MARK: - Onboarding pager + +private let backButtonHeight: CGFloat = 44 + +struct ConnectOnboardingView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.verticalSizeClass) private var verticalSizeClass + @State private var currentPage = 0 + @State private var showConnectViaLink = false + @State private var showInviteSomeone = false + @State private var showCreateAddress = false + + var body: some View { + TabView(selection: $currentPage) { + talkToSomeonePage.tag(0) + connectWithSomeonePage.tag(1) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .sheet(isPresented: $showConnectViaLink) { + NavigationView { + NewChatView(selection: .connect, showQRCodeScanner: true, onboarding: true) + .modifier(ThemedBackground(grouped: true)) + } + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + .sheet(isPresented: $showInviteSomeone) { + NavigationView { + NewChatView(selection: .invite, onboarding: true) + .modifier(ThemedBackground(grouped: true)) + } + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + .sheet(isPresented: $showCreateAddress) { + NavigationView { + UserAddressView(autoCreate: true, onboarding: true) + .modifier(ThemedBackground(grouped: true)) + } + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + } + + @ViewBuilder + private func cardPair( + _ geo: GeometryProxy, + @ViewBuilder card1: () -> C1, + @ViewBuilder card2: () -> C2 + ) -> some View { + let padding: CGFloat = 20 + let spacing: CGFloat = 20 + let isLandscape = verticalSizeClass == .compact + let cardWidth = isLandscape + ? (geo.size.width - padding * 2 - spacing) / 2 + : geo.size.width - padding * 2 + let maxCardHeight = cardWidth * 0.75 + + if isLandscape { + HStack(spacing: spacing) { + card1().frame(maxHeight: maxCardHeight) + card2().frame(maxHeight: maxCardHeight) + } + .padding(.horizontal, padding) + } else { + VStack(spacing: spacing) { + card1().frame(maxHeight: maxCardHeight) + card2().frame(maxHeight: maxCardHeight) + } + .padding(.horizontal, padding) + } + } + + // MARK: Screen 1 + + @ViewBuilder + private func pageHeader(_ title: LocalizedStringKey, showBack: Bool) -> some View { + let isLandscape = verticalSizeClass == .compact + let titleView = Text(title) + .font(.largeTitle) + .bold() + .lineLimit(1) + .minimumScaleFactor(0.75) + .frame(maxWidth: .infinity, alignment: .center) + if isLandscape { + ZStack(alignment: .leading) { + if showBack { backButton } + titleView + } + .padding(.horizontal, 16) + } else { + VStack(spacing: 0) { + if showBack { + backButton.frame(maxWidth: .infinity, alignment: .leading) + } else { + Color.clear.frame(height: backButtonHeight) + } + titleView + } + .padding(.horizontal, 16) + } + } + + private var backButton: some View { + Button { + withAnimation { currentPage = 0 } + } label: { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + Text("Back") + } + } + .frame(height: backButtonHeight) + } + + // MARK: Screen 1 + + private var talkToSomeonePage: some View { + GeometryReader { geo in + VStack(spacing: 0) { + pageHeader("Talk to someone", showBack: false) + + Spacer(minLength: 16) + + cardPair(geo) { + OnboardingCardView( + imageName: "card-let-someone-connect-to-you-alpha", + icon: "link.badge.plus", + title: "Let someone connect to you", + labelHeightRatio: 0.132, + action: { withAnimation { currentPage = 1 } } + ) + } card2: { + OnboardingCardView( + imageName: "card-connect-via-link-alpha", + icon: "qrcode.viewfinder", + title: "Connect via link or QR code", + labelHeightRatio: 0.132, + action: { showConnectViaLink = true } + ) + } + + Spacer(minLength: 16) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + // MARK: Screen 2 + + private var connectWithSomeonePage: some View { + GeometryReader { geo in + VStack(spacing: 0) { + pageHeader("Connect with someone", showBack: true) + + Spacer(minLength: 16) + + cardPair(geo) { + OnboardingCardView( + imageName: "card-invite-someone-privately-alpha", + icon: "link.badge.plus", + title: "Invite someone privately", + subtitle: "A link for one person to connect", + labelHeightRatio: 0.195, + action: { showInviteSomeone = true } + ) + } card2: { + OnboardingCardView( + imageName: "card-create-your-public-address-alpha", + icon: "qrcode", + title: m.userAddress != nil ? "Your public address" : "Create your public address", + subtitle: "For anyone to reach you", + labelHeightRatio: 0.195, + action: { showCreateAddress = true } + ) + } + + Spacer(minLength: 16) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } +} diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift deleted file mode 100644 index f22d59fcac..0000000000 --- a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// AddressCreationCard.swift -// SimpleX (iOS) -// -// Created by Diogo Cunha on 13/11/2024. -// Copyright © 2024 SimpleX Chat. All rights reserved. -// -// Spec: spec/client/navigation.md - -import SwiftUI -import SimpleXChat - -struct AddressCreationCard: View { - @EnvironmentObject var theme: AppTheme - @EnvironmentObject private var chatModel: ChatModel - @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize - @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false - @State private var showAddressCreationAlert = false - @State private var showAddressSheet = false - @State private var showAddressInfoSheet = false - - var body: some View { - let addressExists = chatModel.userAddress != nil - let chats = chatModel.chats.filter { chat in - !chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard - } - ZStack(alignment: .topTrailing) { - HStack(alignment: .top, spacing: 16) { - let envelopeSize = dynamicSize(userFont).profileImageSize - Image(systemName: "envelope.circle.fill") - .resizable() - .frame(width: envelopeSize, height: envelopeSize) - .foregroundColor(.accentColor) - VStack(alignment: .leading) { - Text("Your SimpleX address") - .font(.title3) - Spacer() - Text("How to use it") + textSpace + Text(Image(systemName: "info.circle")).foregroundColor(theme.colors.secondary) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - VStack(alignment: .trailing) { - Image(systemName: "multiply") - .foregroundColor(theme.colors.secondary) - .onTapGesture { - showAddressCreationAlert = true - } - Spacer() - Text("Create") - .foregroundColor(.accentColor) - .onTapGesture { - showAddressSheet = true - } - } - } - .onTapGesture { - showAddressInfoSheet = true - } - .padding() - .background(theme.appColors.sentMessage) - .cornerRadius(12) - .frame(height: dynamicSize(userFont).rowHeight) - .alert(isPresented: $showAddressCreationAlert) { - Alert( - title: Text("SimpleX address"), - message: Text("Tap Create SimpleX address in the menu to create it later."), - dismissButton: .default(Text("Ok")) { - withAnimation { - addressCreationCardShown = true - } - } - ) - } - .sheet(isPresented: $showAddressSheet) { - NavigationView { - UserAddressView(autoCreate: true) - .navigationTitle("SimpleX address") - .navigationBarTitleDisplayMode(.large) - .modifier(ThemedBackground(grouped: true)) - } - } - .sheet(isPresented: $showAddressInfoSheet) { - NavigationView { - UserAddressLearnMore(showCreateAddressButton: true) - .navigationTitle("Address or 1-time link?") - .navigationBarTitleDisplayMode(.inline) - .modifier(ThemedBackground(grouped: true)) - } - } - .onChange(of: addressExists) { exists in - if exists, !addressCreationCardShown { - addressCreationCardShown = true - } - } - .onChange(of: chats.count) { size in - if size >= 3, !addressCreationCardShown { - addressCreationCardShown = true - } - } - .onAppear { - if addressExists, !addressCreationCardShown { - addressCreationCardShown = true - } - } - } -} - -#Preview { - AddressCreationCard() -} diff --git a/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift b/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift new file mode 100644 index 0000000000..460ab9b141 --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift @@ -0,0 +1,113 @@ +// +// ConnectBannerCard.swift +// SimpleX (iOS) +// +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private let bannerImageRatio: CGFloat = 800 / 505 + +struct ConnectBannerCard: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false + @State private var showNewLink = false + @State private var showPasteLink = false + + var body: some View { + VStack(alignment: .trailing, spacing: 3) { + Button { + withAnimation { addressCreationCardShown = true } + } label: { + Image(systemName: "multiply") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(theme.colors.secondary) + .frame(width: 30, height: 30) + .background(theme.colors.onBackground.opacity(0.08), in: Circle()) + } + HStack(spacing: 2) { + bannerHalf( + imageName: "banner-create-link", + icon: "link.badge.plus", + title: "New 1-time link", + action: { showNewLink = true } + ) + bannerHalf( + imageName: "banner-paste-link", + icon: "qrcode.viewfinder", + title: "Paste link / Scan", + action: { showPasteLink = true } + ) + } + .clipShape(RoundedRectangle(cornerRadius: 18)) + } + .sheet(isPresented: $showNewLink) { + NavigationView { + NewChatView(selection: .invite) + .modifier(ThemedBackground(grouped: true)) + } + } + .sheet(isPresented: $showPasteLink) { + NavigationView { + NewChatView(selection: .connect, showQRCodeScanner: true) + .modifier(ThemedBackground(grouped: true)) + } + } + } + + @ViewBuilder + private func bannerHalf(imageName: String, icon: String, title: LocalizedStringKey, action: @escaping () -> Void) -> some View { + Button(action: action) { + VStack(spacing: 0) { + #if SIMPLEX_ASSETS + Image(colorScheme == .light ? imageName : "\(imageName)-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + #else + gradientFallback(icon: icon) + #endif + HStack(spacing: 8) { + #if SIMPLEX_ASSETS + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(theme.colors.primary) + #endif + Text(title) + .font(.footnote) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + .minimumScaleFactor(0.75) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .background(ToolbarMaterial.material(toolbarMaterial)) + } + } + .buttonStyle(.plain) + } + + @ViewBuilder + private func gradientFallback(icon: String) -> some View { + let gp = OnboardingCardView.gradientPoints( + aspectRatio: 1 / bannerImageRatio, + scale: colorScheme == .light ? 1.2 : 1.5 + ) + ZStack { + LinearGradient( + stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops, + startPoint: gp.start, + endPoint: gp.end + ) + Image(systemName: icon) + .font(.system(size: 40)) + .foregroundColor(theme.colors.primary) + } + .aspectRatio(bannerImageRatio, contentMode: .fit) + .frame(maxWidth: .infinity) + } +} diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 8a7ab465d4..b7249f42ea 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -632,6 +632,38 @@ private let versionDescriptions: [VersionDescription] = [ )) ] ), + VersionDescription( + version: "v6.5", + post: URL(string: "https://simplex.chat/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html"), + features: [ + .feature(Description( + icon: nil, + title: "Public channels - speak freely 🚀", + description: nil, + subfeatures: [ + ("antenna.radiowaves.left.and.right", "Reliability: many relays per channel."), + ("server.rack", "Ownership: you can run your own relays."), + ("key.2.on.ring", "Security: owners hold channel keys."), + ("person.badge.shield.checkmark", "Privacy: for owners and subscribers."), + ] + )), + .feature(Description( + icon: "link.badge.plus", + title: "Easier to invite your friends 👋", + description: "We made connecting simpler for new users." + )), + .feature(Description( + icon: "network.badge.shield.half.filled", + title: "Safe web links", + description: "- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." + )), + .feature(Description( + icon: "network", + title: "Non-profit governance", + description: "To make SimpleX Network last." + )) + ] + ), ] private let lastVersion = versionDescriptions.last!.version diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index 44e0b20958..8be0798fb1 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -76,7 +76,7 @@ extension AppSettings { c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get() c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() - c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + c.privacyLinkPreviews = privacyLinkPreviewsGroupDefault.get() c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 6df2d5422e..184b03e679 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -91,6 +91,11 @@ struct DeveloperView: View { UserDefaults.standard.set(val, forKey: def) } } + for def in hintGroupDefaults { + if let val = groupAppDefaults[def] as? Bool { + groupDefaults.set(val, forKey: def) + } + } hintsUnchanged = true } } @@ -98,6 +103,8 @@ struct DeveloperView: View { private func hintDefaultsUnchanged() -> Bool { hintDefaults.allSatisfy { def in appDefaults[def] as? Bool == UserDefaults.standard.bool(forKey: def) + } && hintGroupDefaults.allSatisfy { def in + groupAppDefaults[def] as? Bool == groupDefaults.bool(forKey: def) } } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index eec820833c..3ae9f0eacd 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -13,7 +13,7 @@ struct PrivacySettings: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true - @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true + @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @@ -74,8 +74,8 @@ struct PrivacySettings: View { settingsRow("network", color: theme.colors.secondary) { Toggle("Send link previews", isOn: $useLinkPreviews) .onChange(of: useLinkPreviews) { linkPreviews in - privacyLinkPreviewsGroupDefault.set(linkPreviews) - privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 + UserDefaults.standard.set(linkPreviews, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + privacyLinkPreviewsShowAlertGroupDefault.set(false) } } settingsRow("link", color: theme.colors.secondary) { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index c091224098..65e34a0ac5 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -150,6 +150,10 @@ let hintDefaults = [ DEFAULT_SHOW_DELETE_CONTACT_NOTICE ] +let hintGroupDefaults = [ + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT +] + // not used anymore enum ConnectViaLinkTab: String { case scan diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index d40ec116f4..4df58f8b0c 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -11,11 +11,13 @@ import MessageUI @preconcurrency import SimpleXChat struct UserAddressView: View { + @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false @State var autoCreate = false + var onboarding: Bool = false @State private var showShortLink = true @State private var settings = AddressSettingsState() @State private var savedSettings = AddressSettingsState() @@ -54,6 +56,14 @@ struct UserAddressView: View { } } } + .if(onboarding) { v in + v.toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Image(systemName: "info.circle").opacity(0) + } + } + .navigationBarTitleDisplayMode(.inline) + } .onAppear { if chatModel.userAddress == nil, autoCreate { createAddress() @@ -64,12 +74,16 @@ struct UserAddressView: View { private func userAddressView() -> some View { List { if let userAddress = chatModel.userAddress { - existingAddressView(userAddress) - .onAppear { - settings = AddressSettingsState(settings: userAddress.addressSettings) - savedSettings = AddressSettingsState(settings: userAddress.addressSettings) - } - } else { + if onboarding { + onboardingAddressView(userAddress) + } else { + existingAddressView(userAddress) + .onAppear { + settings = AddressSettingsState(settings: userAddress.addressSettings) + savedSettings = AddressSettingsState(settings: userAddress.addressSettings) + } + } + } else if !onboarding { Section { createAddressButton() } header: { @@ -121,8 +135,8 @@ struct UserAddressView: View { ) case .shareOnCreate: return Alert( - title: Text("Share address with contacts?"), - message: Text("Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts."), + title: Text("Share address with SimpleX contacts?"), + message: Text("Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts."), primaryButton: .default(Text("Share")) { setProfileAddress($progressIndicator, true) shareViaProfile = true @@ -157,7 +171,19 @@ struct UserAddressView: View { } addressSettingsButton(userAddress) } header: { + #if SIMPLEX_ASSETS + VStack(alignment: .leading, spacing: 0) { + Image(colorScheme == .light ? "simplex-address-small" : "simplex-address-small-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + .padding(.top, -20) + ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) + } + .padding(.bottom, 4) + #else ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) + #endif } footer: { if settings.businessAddress { Text("Add your team members to the conversations.") @@ -184,6 +210,59 @@ struct UserAddressView: View { } } + @ViewBuilder private func onboardingAddressView(_ userAddress: UserContactLink) -> some View { + Section { + HStack(spacing: 8) { + let link = userAddress.connLinkContact.simplexChatUri(short: showShortLink) + linkTextView(link) + Button { UIPasteboard.general.string = link } label: { + Image(systemName: "doc.on.doc") + .padding(.top, -7) + .padding(.horizontal, 8) + } + Button { showShareSheet(items: [link]) } label: { + Image(systemName: "square.and.arrow.up") + .padding(.top, -7) + .padding(.horizontal, 8) + } + } + .frame(maxWidth: .infinity) + } header: { + #if SIMPLEX_ASSETS + VStack(alignment: .leading) { + Image(colorScheme == .light ? "simplex-address" : "simplex-address-light") + .resizable() + .scaledToFit() + .frame(maxWidth: .infinity) + Text("Use this address in your social media profile, website, or email signature.") + .font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } + .padding(.bottom, 4) + #else + Text("Use this address in your social media profile, website, or email signature.") + .font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + .padding(.bottom, 6) + #endif + } + .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) + + Section { + SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink) + .id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))") + .padding() + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(uiColor: .secondarySystemGroupedBackground)) + ) + .padding(.horizontal) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + } header: { + Text("Or use this QR - print or show online.").font(.body).foregroundColor(theme.colors.onBackground).textCase(nil) + } + } + private func createAddressButton() -> some View { Button { createAddress() @@ -200,9 +279,22 @@ struct UserAddressView: View { DispatchQueue.main.async { if let connLinkContact { chatModel.userAddress = UserContactLink(connLinkContact) - alert = .shareOnCreate + let hasRelevantContacts = chatModel.chats.contains { chat in + if case let .direct(contact) = chat.chatInfo { + return contact.active && !contact.isContactCard && !contact.contactConnIncognito + } + return false + } + if hasRelevantContacts { + alert = .shareOnCreate + progressIndicator = false + } else { + setProfileAddress($progressIndicator, true) + shareViaProfile = true + } + } else { + progressIndicator = false } - progressIndicator = false } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") @@ -486,15 +578,15 @@ struct UserAddressSettingsView: View { private func shareWithContactsButton() -> some View { settingsRow("person", color: theme.colors.secondary) { - Toggle("Share with contacts", isOn: $shareViaProfile) + Toggle("Share with SimpleX contacts", isOn: $shareViaProfile) .onChange(of: shareViaProfile) { on in if ignoreShareViaProfileChange { ignoreShareViaProfileChange = false } else { if on { showAlert( - NSLocalizedString("Share address with contacts?", comment: "alert title"), - message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + NSLocalizedString("Share address with SimpleX contacts?", comment: "alert title"), + message: NSLocalizedString("Profile update will be sent to your SimpleX contacts.", comment: "alert message"), actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), @@ -516,7 +608,7 @@ struct UserAddressSettingsView: View { } else { showAlert( NSLocalizedString("Stop sharing address?", comment: "alert title"), - message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + message: NSLocalizedString("Profile update will be sent to your SimpleX contacts.", comment: "alert message"), actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 569b5caf13..2e609c3f7d 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -26,7 +26,7 @@ struct UserProfile: View { var body: some View { List { - EditProfileImage(profileImage: $profile.image, showChooseSource: $showChooseSource) + EditProfileImage(profileImage: $profile.image, iconName: "person.crop.circle.fill", showChooseSource: $showChooseSource) .padding(.top) Section { @@ -178,6 +178,7 @@ struct EditProfileImage: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner @Binding var profileImage: String? + var iconName: String @Binding var showChooseSource: Bool var body: some View { @@ -193,7 +194,7 @@ struct EditProfileImage: View { } } else { ZStack(alignment: .center) { - ProfileImage(imageStr: profileImage, size: 160) + ProfileImage(imageStr: profileImage, iconName: iconName, size: 160) editImageButton { showChooseSource = true } } } 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 d1bb5fe716..3cf65c8b54 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -185,6 +185,10 @@ %d месеца time interval + + %d relays + channel relay bar + %d sec %d сек. @@ -200,11 +204,37 @@ %d пропуснато(и) съобщение(я) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d седмици time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld избран(и) контакт(а) @@ -365,6 +399,10 @@ **Сканирай / Постави линк**: за свързване чрез получения линк. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Внимание**: Незабавните push известия изискват парола, запазена в Keychain. @@ -1315,6 +1353,10 @@ swipe action Блокирай члена? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Блокиран от админ @@ -1365,6 +1407,10 @@ swipe action И вие, и вашият контакт можете да изпращате гласови съобщения. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Български, финландски, тайландски и украински - благодарение на потребителите и [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1552,6 +1598,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Чат @@ -1637,6 +1727,22 @@ set passcode view Потребителски профил No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Тема на чата @@ -1687,6 +1793,14 @@ set passcode view Проверявай за съобщенията, когато е разрешено. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Проверете адреса на сървъра и опитайте отново. @@ -1832,6 +1946,10 @@ set passcode view Конфигурирай ICE сървъри No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Конфигуриране на сървърни оператори @@ -1895,7 +2013,8 @@ set passcode view Connect Свързване - server test step + relay test step +server test step Connect automatically @@ -2164,7 +2283,7 @@ This is your own one-time link! Correct name to %@? Поправи име на %@? - No comment provided by engineer. + alert message Create @@ -2220,6 +2339,14 @@ This is your own one-time link! Създай профил No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Създай опашка @@ -2253,6 +2380,10 @@ This is your own one-time link! Създаване на архивен линк No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Линкът се създава… @@ -2410,6 +2541,10 @@ This is your own one-time link! Децентрализиран No comment provided by engineer. + + Decode link + relay test step + Decryption error Грешка при декриптиране @@ -2455,6 +2590,14 @@ swipe action Изтрий и уведоми контакт No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2617,6 +2760,10 @@ alert button Изтрий опашка server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2964,6 +3111,10 @@ chat item action Редактирай chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Редактирай групов профил @@ -2997,6 +3148,10 @@ chat item action Активирай TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Активиране на автоматично изтриване на съобщения? @@ -3139,6 +3294,10 @@ chat item action Въведете kодa за достъп No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Въведи правилна парола. @@ -3164,6 +3323,10 @@ chat item action Въведете парола по-горе, за да се покаже! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Въведи сървъра ръчно @@ -3217,6 +3380,10 @@ chat item action Грешка при добавяне на член(ове) No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3269,6 +3436,10 @@ chat item action Грешка при създаване на адрес No comment provided by engineer. + + Error creating channel + alert title + Error creating group Грешка при създаване на група @@ -3398,10 +3569,6 @@ chat item action Грешка при отваряне на чата No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Грешка при получаване на файл @@ -3441,6 +3608,10 @@ chat item action Грешка при запазване на ICE сървърите No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3579,7 +3750,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3802,7 +3974,8 @@ snd error text Fingerprint in server address does not match certificate. Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3844,7 +4017,8 @@ snd error text For chat profile %@: - servers error + servers error +servers warning For console @@ -3969,6 +4143,10 @@ Error: %2$@ GIF файлове и стикери No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. @@ -4448,7 +4626,15 @@ More improvements are coming soon! Invalid name! Невалидно име! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4558,6 +4744,10 @@ More improvements are coming soon! присъединяване като %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Влез в групата @@ -4643,6 +4833,14 @@ This is your link for group %@! Напусни swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4915,6 +5113,10 @@ This is your link for group %@! Чернова на съобщение No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -5211,6 +5413,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Нова заявка за контакт @@ -5281,6 +5487,14 @@ This is your link for group %@! Приложението няма kод за достъп Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5413,6 +5627,10 @@ This is your link for group %@! Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Несъвместим! @@ -5615,6 +5833,10 @@ Requires compatible VPN. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat Отвори чат @@ -5651,6 +5873,10 @@ Requires compatible VPN. Отвори миграцията към друго устройство authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5730,6 +5956,14 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING бройка @@ -5928,6 +6162,14 @@ Error: %@ Запазете последната чернова на съобщението с прикачени файлове. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Предварително зададен адрес на сървъра @@ -6002,6 +6244,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Профилни и сървърни връзки @@ -6328,6 +6574,22 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес. @@ -6338,6 +6600,10 @@ swipe action Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Премахване @@ -6374,6 +6640,14 @@ swipe action Премахване на паролата от keychain? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6610,6 +6884,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6633,6 +6911,14 @@ chat item action Запази и актуализирай профила на групата No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Запази профила на групата @@ -7055,6 +7341,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Сървърът изисква оторизация за създаване на опашки, проверете паролата @@ -7233,6 +7523,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Сподели този еднократен линк за връзка @@ -7404,8 +7698,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7568,6 +7862,23 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7656,6 +7967,10 @@ report reason Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group No comment provided by engineer. @@ -7702,12 +8017,17 @@ report reason Test failed at step %@. Тестът е неуспешен на стъпка %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Тествай сървър @@ -7758,6 +8078,10 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7936,6 +8260,14 @@ It can happen because of some bug or when the connection is compromised.Тази група вече не съществува. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8137,6 +8469,10 @@ You will be prompted to complete authentication before this feature is enabled.< Отблокирай член? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8369,6 +8705,10 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Използвай за нови връзки @@ -8406,6 +8746,10 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Използвай сървър @@ -8441,6 +8785,10 @@ To connect, please ask your contact to create another connection link and check Използват се сървърите на SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Потвърди кода с настолното устройство @@ -8558,6 +8906,14 @@ To connect, please ask your contact to create another connection link and check Гласово съобщение… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Изчакване на настолно устройство… @@ -8843,6 +9199,10 @@ Repeat join request? Можете да зададете визуализация на известията на заключен екран през настройките. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Можете да споделите линк или QR код - всеки ще може да се присъедини към групата. Няма да загубите членовете на групата, ако по-късно я изтриете. @@ -8886,6 +9246,10 @@ Repeat join request? Не може да изпращате съобщения! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Не можахте да бъдете потвърдени; Моля, опитайте отново. @@ -8993,6 +9357,10 @@ Repeat connection request? Все още ще получавате обаждания и известия от заглушени профили, когато са активни. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -9036,6 +9404,10 @@ Repeat connection request? Вашите обаждания No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Вашата база данни @@ -9115,6 +9487,11 @@ Repeat connection request? Вашият профил No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Вашият профил **%@** ще бъде споделен. @@ -9139,6 +9516,14 @@ Repeat connection request? Вашият автоматично генериран профил No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Вашият адрес на сървъра @@ -9183,6 +9568,10 @@ Repeat connection request? по-горе, след това избери: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -9200,6 +9589,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin админ @@ -9341,6 +9734,14 @@ marked deleted chat item preview text промяна на адреса… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored цветен @@ -9482,6 +9883,10 @@ pref value изтрит deleted chat item + + deleted channel + rcv group event chat item + deleted contact изтрит контакт @@ -9591,6 +9996,10 @@ pref value грешка No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9715,6 +10124,10 @@ pref value напусна rcv group event chat item + + link + No comment provided by engineer. + marked deleted маркирано като изтрито @@ -9782,6 +10195,10 @@ pref value никога delete after time + + new + No comment provided by engineer. + new message ново съобщение @@ -9897,6 +10314,10 @@ time to disappear отхвърлено повикване call status + + relay + member role + removed отстранен @@ -9907,6 +10328,10 @@ time to disappear отстранен %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address премахнат адрес за контакт @@ -10047,6 +10472,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile актуализиран профил на групата @@ -10067,6 +10496,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link чрез линк с адрес за контакт @@ -10139,6 +10572,10 @@ last received msg: %2$@ вие сте наблюдател No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ вие блокирахте %@ 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 b212f42592..e7d0fc2d4b 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -185,6 +185,10 @@ %d měsíce time interval + + %d relays + channel relay bar + %d sec %d sek @@ -200,11 +204,37 @@ %d přeskočené zprávy integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d týdnů time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld kontakt(y) vybrané @@ -227,6 +261,7 @@ %lld group events + %lld událostí skupiny No comment provided by engineer. @@ -363,6 +398,10 @@ **Skenovat / Vložit odkaz**: pro připojení pomocí odkazu který jste obdrželi. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence. @@ -515,7 +554,7 @@ time interval A separate TCP connection will be used **for each chat profile you have in the app**. - Samostatné připojení TCP bude použito **pro každý chat profil, který máte v aplikaci**. + Samostatné připojení TCP bude použito **pro každý profil chatu, který máte v aplikaci**. No comment provided by engineer. @@ -764,7 +803,7 @@ swipe action All chats will be removed from the list %@, and the list deleted. - Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn. + Všechny chaty budou odstraněny ze seznamu %@ a seznam bude smazán. alert message @@ -982,7 +1021,7 @@ swipe action An empty chat profile with the provided name is created, and the app opens as usual. - Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle. + Vytvořit prázdný profil chatu se zadaným názvem a otevřít aplikaci jako obvykle. No comment provided by engineer. @@ -1281,6 +1320,10 @@ swipe action Blokovat člena? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Blokován správcem @@ -1329,6 +1372,10 @@ swipe action Hlasové zprávy můžete posílat vy i váš kontakt. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulharský, finský, thajský a ukrajinský - díky uživatelům a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1353,7 +1400,7 @@ swipe action By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). - Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). + Podle profilu chatu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. @@ -1461,7 +1508,7 @@ new chat action Change chat profiles - Změnit chat profily + Změnit profily chatu authentication reason @@ -1510,6 +1557,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat No comment provided by engineer. @@ -1589,6 +1680,22 @@ set passcode view Profil uživatele No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme No comment provided by engineer. @@ -1630,6 +1737,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Zkontrolujte adresu serveru a zkuste to znovu. @@ -1757,6 +1872,10 @@ set passcode view Konfigurace serverů ICE No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators No comment provided by engineer. @@ -1814,7 +1933,8 @@ set passcode view Connect Připojit - server test step + relay test step +server test step Connect automatically @@ -2060,7 +2180,7 @@ Toto je váš vlastní jednorázový odkaz! Correct name to %@? - No comment provided by engineer. + alert message Create @@ -2113,6 +2233,14 @@ Toto je váš vlastní jednorázový odkaz! Vytvořte si profil No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Vytvořit frontu @@ -2143,6 +2271,10 @@ Toto je váš vlastní jednorázový odkaz! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2299,6 +2431,10 @@ Toto je váš vlastní jednorázový odkaz! Decentralizované No comment provided by engineer. + + Decode link + relay test step + Decryption error Chyba dešifrování @@ -2342,6 +2478,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2352,12 +2496,12 @@ swipe action Delete chat profile - Smazat chat profil + Smazat profil chatu No comment provided by engineer. Delete chat profile? - Smazat chat profil? + Smazat profil chatu? No comment provided by engineer. @@ -2503,6 +2647,10 @@ alert button Odstranit frontu server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2840,6 +2988,10 @@ chat item action Upravit chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Upravit profil skupiny @@ -2873,6 +3025,10 @@ chat item action Povolit TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Povolit automatické mazání zpráv? @@ -3009,6 +3165,10 @@ chat item action Zadat heslo No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Zadejte správnou přístupovou frázi. @@ -3032,6 +3192,10 @@ chat item action Zadejte heslo do hledání! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Zadejte server ručně @@ -3083,6 +3247,10 @@ chat item action Chyba přidávání člena(ů) No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3135,6 +3303,10 @@ chat item action Chyba při vytváření adresy No comment provided by engineer. + + Error creating channel + alert title + Error creating group Chyba při vytváření skupiny @@ -3261,10 +3433,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Chyba při příjmu souboru @@ -3304,6 +3472,10 @@ chat item action Chyba při ukládání serverů ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3438,7 +3610,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3655,7 +3828,8 @@ snd error text Fingerprint in server address does not match certificate. Otisk certifikátu v adrese serveru neodpovídá. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3697,7 +3871,8 @@ snd error text For chat profile %@: - servers error + servers error +servers warning For console @@ -3816,6 +3991,10 @@ Error: %2$@ GIFy a nálepky No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. @@ -3945,7 +4124,7 @@ Error: %2$@ Hidden chat profiles - Skryté chat profily + Skryté profily chatu No comment provided by engineer. @@ -4279,7 +4458,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4388,6 +4575,10 @@ More improvements are coming soon! připojit se jako %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Připojit ke skupině @@ -4467,6 +4658,14 @@ This is your link for group %@! Opustit swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4735,6 +4934,10 @@ This is your link for group %@! Návrh zprávy No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -4933,7 +5136,7 @@ This is your link for group %@! Multiple chat profiles - Více chatovacích profilů + Více profilů chatu No comment provided by engineer. @@ -5015,6 +5218,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Žádost o nový kontakt @@ -5085,6 +5292,14 @@ This is your link for group %@! Žádné heslo aplikace Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5216,6 +5431,10 @@ This is your link for group %@! Bez uživatelských identifikátorů. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5416,6 +5635,10 @@ Vyžaduje povolení sítě VPN. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat Otevřete chat @@ -5450,6 +5673,10 @@ Vyžaduje povolení sítě VPN. Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5523,6 +5750,14 @@ Vyžaduje povolení sítě VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count Počet PING @@ -5714,6 +5949,14 @@ Error: %@ Zachování posledního návrhu zprávy s přílohami. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Přednastavená adresa serveru @@ -5787,6 +6030,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profil a připojení k serveru @@ -5879,7 +6126,7 @@ Enable in *Network & servers* settings. Protect your chat profiles with a password! - Chraňte své chat profily heslem! + Chraňte své profily chatu pomocí hesla! No comment provided by engineer. @@ -5947,6 +6194,7 @@ Enable in *Network & servers* settings. Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + Více informací v [průvodci uživatele](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. @@ -6106,6 +6354,22 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Přenosový server se používá pouze v případě potřeby. Jiná strana může sledovat vaši IP adresu. @@ -6116,6 +6380,10 @@ swipe action Přenosový server chrání vaši IP adresu, ale může sledovat dobu trvání hovoru. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Odstranit @@ -6152,6 +6420,14 @@ swipe action Odstranit přístupovou frázi z klíčenek? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6274,7 +6550,7 @@ swipe action Restart the app to create a new chat profile - Restartujte aplikaci pro vytvoření nového chat profilu + Restartujte aplikaci pro vytvoření nového profilu chatu No comment provided by engineer. @@ -6383,6 +6659,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6406,6 +6686,14 @@ chat item action Uložit a aktualizovat profil skupiny No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Uložení profilu skupiny @@ -6711,7 +6999,7 @@ chat item action Sending delivery receipts will be enabled for all contacts in all visible chat profiles. - Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných chat profilech. + Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu. No comment provided by engineer. @@ -6821,6 +7109,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo. @@ -6996,6 +7288,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -7162,8 +7458,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7322,6 +7618,23 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7410,6 +7723,10 @@ report reason Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group No comment provided by engineer. @@ -7453,12 +7770,17 @@ report reason Test failed at step %@. Test selhal v kroku %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Testovací server @@ -7509,6 +7831,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7603,7 +7929,7 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The servers for new connections of your current chat profile **%@**. - Servery pro nová připojení vašeho aktuálního chat profilu **%@**. + Servery pro nová připojení vašeho aktuálního profilu chatu **%@**. No comment provided by engineer. @@ -7681,6 +8007,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Tato skupina již neexistuje. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7695,7 +8029,7 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován This setting applies to messages in your current chat profile **%@**. - Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**. + Toto nastavení platí pro zprávy ve vašem aktuálním profilu chatu **%@**. No comment provided by engineer. @@ -7773,7 +8107,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Chat profily**. + Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Profily chatu**. No comment provided by engineer. @@ -7875,6 +8209,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -7896,7 +8234,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unhide chat profile - Odkrýt chat profil + Odkrýt profil chatu No comment provided by engineer. @@ -8102,6 +8440,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Použít pro nová připojení @@ -8137,6 +8479,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Použít server @@ -8171,6 +8517,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Používat servery SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8280,6 +8630,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Hlasová zpráva… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... No comment provided by engineer. @@ -8546,6 +8904,10 @@ Repeat join request? Náhled oznámení na zamykací obrazovce můžete změnit v nastavení. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud ji později odstraníte. @@ -8588,6 +8950,10 @@ Repeat join request? Nemůžete posílat zprávy! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Nemohli jste být ověřeni; Zkuste to prosím znovu. @@ -8692,6 +9058,10 @@ Repeat connection request? Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8735,6 +9105,10 @@ Repeat connection request? Vaše hovory No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Vaše chatovací databáze @@ -8751,7 +9125,7 @@ Repeat connection request? Your chat profiles - Vaše chat profily + Vaše profily chatu No comment provided by engineer. @@ -8813,6 +9187,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Váš profil **%@** bude sdílen. @@ -8837,6 +9216,14 @@ Repeat connection request? Váš náhodný profil No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Adresa vašeho serveru @@ -8881,6 +9268,10 @@ Repeat connection request? výše, pak vyberte: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8898,6 +9289,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin správce @@ -9032,6 +9427,14 @@ marked deleted chat item preview text změna adresy… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored barevné @@ -9172,6 +9575,10 @@ pref value smazáno deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9280,6 +9687,10 @@ pref value chyba No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9403,6 +9814,10 @@ pref value opustil rcv group event chat item + + link + No comment provided by engineer. + marked deleted označeno jako smazáno @@ -9469,6 +9884,10 @@ pref value nikdy delete after time + + new + No comment provided by engineer. + new message nová zpráva @@ -9582,6 +10001,10 @@ time to disappear odmítnutý hovor call status + + relay + member role + removed odstraněno @@ -9592,6 +10015,10 @@ time to disappear odstraněno %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address profile update event chat item @@ -9723,6 +10150,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile aktualizoval profil skupiny @@ -9741,6 +10172,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link prostřednictvím odkazu na kontaktní adresu @@ -9812,6 +10247,10 @@ last received msg: %2$@ jste pozorovatel No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item 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 7ab0535158..5840cff078 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -185,6 +185,10 @@ %d Monate time interval + + %d relays + channel relay bar + %d sec %d s @@ -200,11 +204,37 @@ %d übersprungene Nachricht(en) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d Wochen time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld Kontakt(e) ausgewählt @@ -365,6 +399,10 @@ **Link scannen / einfügen**: Um eine Verbindung über den Link herzustellen, den Sie erhalten haben. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist. @@ -1317,6 +1355,10 @@ swipe action Mitglied blockieren? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin wurde vom Administrator blockiert @@ -1367,6 +1409,10 @@ swipe action Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgarisch, Finnisch, Thailändisch und Ukrainisch - Dank der Nutzer und [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1554,6 +1600,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Chat @@ -1639,6 +1729,22 @@ set passcode view Benutzerprofil No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Chat-Design @@ -1689,6 +1795,14 @@ set passcode view Wenn es erlaubt ist, Nachrichten überprüfen. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Überprüfen Sie die Serveradresse und versuchen Sie es nochmal. @@ -1834,6 +1948,10 @@ set passcode view ICE-Server konfigurieren No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Server-Betreiber konfigurieren @@ -1897,7 +2015,8 @@ set passcode view Connect Verbinden - server test step + relay test step +server test step Connect automatically @@ -2025,6 +2144,7 @@ Das ist Ihr eigener Einmal-Link! Connection failed + Verbindung fehlgeschlagen No comment provided by engineer. @@ -2177,7 +2297,7 @@ Das ist Ihr eigener Einmal-Link! Correct name to %@? Richtiger Name für %@? - No comment provided by engineer. + alert message Create @@ -2234,6 +2354,14 @@ Das ist Ihr eigener Einmal-Link! Profil erstellen No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Erzeuge Warteschlange @@ -2269,6 +2397,10 @@ Das ist Ihr eigener Einmal-Link! Archiv-Link erzeugen No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Link wird erstellt… @@ -2432,6 +2564,10 @@ Das ist Ihr eigener Einmal-Link! Dezentral No comment provided by engineer. + + Decode link + relay test step + Decryption error Entschlüsselungsfehler @@ -2478,6 +2614,14 @@ swipe action Kontakt löschen und benachrichtigen No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Chat löschen @@ -2649,6 +2793,10 @@ alert button Lösche Warteschlange server test step + + Delete relay + No comment provided by engineer. + Delete report Meldung löschen @@ -3025,6 +3173,10 @@ chat item action Bearbeiten chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Gruppenprofil bearbeiten @@ -3060,6 +3212,10 @@ chat item action TCP-Keep-alive aktivieren No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Automatisches Löschen von Nachrichten aktivieren? @@ -3205,6 +3361,10 @@ chat item action Zugangscode eingeben No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Geben Sie das korrekte Passwort ein. @@ -3230,6 +3390,10 @@ chat item action Für die Anzeige das Passwort im Suchfeld eingeben! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Geben Sie den Server manuell ein @@ -3285,6 +3449,10 @@ chat item action Fehler beim Hinzufügen von Mitgliedern No comment provided by engineer. + + Error adding relay + alert title + Error adding server Fehler beim Hinzufügen des Servers @@ -3345,6 +3513,10 @@ chat item action Fehler beim Erstellen der Adresse No comment provided by engineer. + + Error creating channel + alert title + Error creating group Fehler beim Erzeugen der Gruppe @@ -3480,11 +3652,6 @@ chat item action Fehler beim Öffnen des Chat No comment provided by engineer. - - Error opening group - Fehler beim Vorbereiten der Gruppe - No comment provided by engineer. - Error receiving file Fehler beim Herunterladen der Datei @@ -3530,6 +3697,10 @@ chat item action Fehler beim Speichern der ICE-Server No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Fehler beim Speichern der Chat-Liste @@ -3675,7 +3846,8 @@ snd error text Error: %@. Fehler: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4091,8 @@ snd error text Fingerprint in server address does not match certificate. Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3964,7 +4137,8 @@ snd error text For chat profile %@: Für das Chat-Profil %@: - servers error + servers error +servers warning For console @@ -4105,6 +4279,10 @@ Fehler: %2$@ GIFs und Sticker No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Bei Erwähnung benachrichtigt werden. @@ -4347,6 +4525,7 @@ Fehler: %2$@ If you joined or created channels, they will stop working permanently. + Kanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert. down migration warning @@ -4606,7 +4785,15 @@ Weitere Verbesserungen sind bald verfügbar! Invalid name! Ungültiger Name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4719,6 +4906,10 @@ Weitere Verbesserungen sind bald verfügbar! Als %@ beitreten No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Treten Sie der Gruppe bei @@ -4806,6 +4997,14 @@ Das ist Ihr Link für die Gruppe %@! Verlassen swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Chat verlassen @@ -5101,6 +5300,10 @@ Das ist Ihr Link für die Gruppe %@! Nachrichtenentwurf No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Nachricht weitergeleitet @@ -5421,6 +5624,10 @@ Das ist Ihr Link für die Gruppe %@! Neue Chat-Erfahrung 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Neue Kontaktanfrage @@ -5496,6 +5703,14 @@ Das ist Ihr Link für die Gruppe %@! Kein App-Passwort Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Keine Chats @@ -5646,6 +5861,10 @@ Das ist Ihr Link für die Gruppe %@! Keine Benutzerkennungen. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Nicht kompatibel! @@ -5861,6 +6080,10 @@ Dies erfordert die Aktivierung eines VPNs. Änderungen öffnen No comment provided by engineer. + + Open channel + new chat action + Open chat Chat öffnen @@ -5901,6 +6124,10 @@ Dies erfordert die Aktivierung eines VPNs. Migration auf ein anderes Gerät öffnen authentication reason + + Open new channel + new chat action + Open new chat Neuen Chat öffnen @@ -5993,6 +6220,14 @@ Dies erfordert die Aktivierung eines VPNs. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING-Zähler @@ -6202,6 +6437,14 @@ Fehler: %@ Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Voreingestellte Serveradresse @@ -6287,6 +6530,10 @@ Fehler: %@ Zeitüberschreitung der privaten Routing-Sitzung alert title + + Proceed + alert action + Profile and server connections Profil und Serververbindungen @@ -6636,6 +6883,22 @@ swipe action Mitglied ablehnen? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden. @@ -6646,6 +6909,10 @@ swipe action Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Entfernen @@ -6686,6 +6953,14 @@ swipe action Passwort aus dem Schlüsselbund entfernen? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Entfernt Nachrichten und blockiert Mitglieder. @@ -6947,6 +7222,10 @@ chat item action Speichern (und Mitglieder benachrichtigen) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Speichern der Aufnahme-Einstellungen? @@ -6972,6 +7251,14 @@ chat item action Gruppen-Profil sichern und aktualisieren No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Gruppenprofil speichern @@ -7432,6 +7719,10 @@ chat item action Das Server-Protokoll wurde geändert. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Der Server erfordert zum Erstellen von Warteschlangen eine Autorisierung. Bitte überprüfen Sie das Passwort. @@ -7628,6 +7919,10 @@ chat item action Profil teilen No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Teilen Sie diesen Einmal-Einladungslink @@ -7813,9 +8108,8 @@ chat item action Die SimpleX-Protokolle wurden von Trail of Bits überprüft. No comment provided by engineer. - - SimpleX relay link - SimpleX Relais-Link + + SimpleX relay address simplex link type @@ -7991,6 +8285,23 @@ report reason Abonniert No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Fehler beim Abonnieren @@ -8091,6 +8402,10 @@ report reason Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Tippen, um der Gruppe beizutreten @@ -8139,13 +8454,18 @@ report reason Test failed at step %@. Der Test ist beim Schritt %@ fehlgeschlagen. - server test failure + relay test failure +server test failure Test notifications Benachrichtigungen testen No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Teste Server @@ -8198,6 +8518,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Durch Verwendung verschiedener Netzwerk-Betreiber für jede Unterhaltung schützt die App Ihre Privatsphäre. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion). @@ -8388,6 +8712,14 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Diese Gruppe existiert nicht mehr. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden. @@ -8610,6 +8942,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Mitglied freigeben? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Nicht ausgelieferte Nachrichten @@ -8862,6 +9198,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Für Nachrichten verwenden No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Für neue Verbindungen nutzen @@ -8902,6 +9242,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Sie nutzen privates Routing mit unbekannten Servern. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Server nutzen @@ -8942,6 +9286,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Verwendung von SimpleX-Chat-Servern. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Code mit dem Desktop überprüfen @@ -9062,6 +9410,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Sprachnachrichten… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Es wird auf den Desktop gewartet... @@ -9364,6 +9720,10 @@ Verbindungsanfrage wiederholen? Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind. @@ -9409,6 +9769,10 @@ Verbindungsanfrage wiederholen? Sie können keine Nachrichten versenden! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut. @@ -9521,6 +9885,10 @@ Verbindungsanfrage wiederholen? Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf bleibt erhalten. @@ -9566,6 +9934,10 @@ Verbindungsanfrage wiederholen? Anrufe No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Chat-Datenbank @@ -9651,6 +10023,11 @@ Verbindungsanfrage wiederholen? Mein Profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Ihr Profil **%@** wird geteilt. @@ -9676,6 +10053,14 @@ Verbindungsanfrage wiederholen? Ihr Zufallsprofil No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Ihre Serveradresse @@ -9721,6 +10106,10 @@ Verbindungsanfrage wiederholen? Danach die gewünschte Aktion auswählen: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ %@ angenommen @@ -9741,6 +10130,10 @@ Verbindungsanfrage wiederholen? hat Sie angenommen rcv group event chat item + + active + No comment provided by engineer. + admin Admin @@ -9887,6 +10280,14 @@ marked deleted chat item preview text Wechsel der Empfängeradresse wurde gestartet… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored farbig @@ -10033,6 +10434,10 @@ pref value Gelöscht deleted chat item + + deleted channel + rcv group event chat item + deleted contact Gelöschter Kontakt @@ -10143,6 +10548,10 @@ pref value Fehler No comment provided by engineer. + + error: %@ + receive error chat item + expired Abgelaufen @@ -10150,6 +10559,7 @@ pref value failed + Fehlgeschlagen No comment provided by engineer. @@ -10272,6 +10682,10 @@ pref value hat die Gruppe verlassen rcv group event chat item + + link + No comment provided by engineer. + marked deleted als gelöscht markiert @@ -10342,6 +10756,10 @@ pref value nie delete after time + + new + No comment provided by engineer. + new message Neue Nachricht @@ -10465,6 +10883,10 @@ time to disappear Abgelehnter Anruf call status + + relay + member role + removed entfernt @@ -10475,6 +10897,10 @@ time to disappear hat %@ aus der Gruppe entfernt rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address Die Kontaktadresse wurde entfernt @@ -10629,6 +11055,10 @@ Zuletzt empfangene Nachricht: %2$@ Ungeschützt No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile Aktualisiertes Gruppenprofil @@ -10649,6 +11079,10 @@ Zuletzt empfangene Nachricht: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link über einen Kontaktadressen-Link @@ -10724,6 +11158,10 @@ Zuletzt empfangene Nachricht: %2$@ Sie sind Beobachter No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ Sie haben %@ blockiert 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 fbc3b2dfa8..88b9939944 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -185,6 +185,11 @@ %d months time interval + + %d relays + %d relays + channel relay bar + %d sec %d sec @@ -200,11 +205,43 @@ %d skipped message(s) integrity error chat item + + %d subscriber + %d subscriber + channel subscriber count + + + %d subscribers + %d subscribers + channel subscriber count + %d weeks %d weeks time interval + + %1$d/%2$d relays active + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +252,11 @@ %lld %@ No comment provided by engineer. + + %lld channel events + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contact(s) selected @@ -365,6 +407,11 @@ **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. + + **Test relay** to retrieve its name. + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Warning**: Instant push notifications require passphrase saved in Keychain. @@ -1317,6 +1364,11 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Blocked by admin @@ -1367,6 +1419,11 @@ swipe action Both you and your contact can send voice messages. No comment provided by engineer. + + Broadcast + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1554,6 +1611,61 @@ new chat action authentication reason set passcode view + + Channel + Channel + No comment provided by engineer. + + + Channel display name + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + Channel full name (optional) + No comment provided by engineer. + + + Channel image + Channel image + No comment provided by engineer. + + + Channel link + Channel link + No comment provided by engineer. + + + Channel profile + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Chat @@ -1639,6 +1751,26 @@ set passcode view Chat profile No comment provided by engineer. + + Chat relay + Chat relay + No comment provided by engineer. + + + Chat relays + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Chat theme @@ -1689,6 +1821,16 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + Check relay address and try again. + alert message + + + Check relay name and try again. + Check relay name and try again. + alert message + Check server address and try again. Check server address and try again. @@ -1834,6 +1976,11 @@ set passcode view Configure ICE servers No comment provided by engineer. + + Configure relays + Configure relays + No comment provided by engineer. + Configure server operators Configure server operators @@ -1897,7 +2044,8 @@ set passcode view Connect Connect - server test step + relay test step +server test step Connect automatically @@ -2178,7 +2326,7 @@ This is your own one-time link! Correct name to %@? Correct name to %@? - No comment provided by engineer. + alert message Create @@ -2235,6 +2383,16 @@ This is your own one-time link! Create profile No comment provided by engineer. + + Create public channel + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + Create public channel (BETA) + No comment provided by engineer. + Create queue Create queue @@ -2270,6 +2428,11 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + Creating channel + No comment provided by engineer. + Creating link… Creating link… @@ -2433,6 +2596,11 @@ This is your own one-time link! Decentralized No comment provided by engineer. + + Decode link + Decode link + relay test step + Decryption error Decryption error @@ -2479,6 +2647,16 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + Delete channel + No comment provided by engineer. + + + Delete channel? + Delete channel? + No comment provided by engineer. + Delete chat Delete chat @@ -2650,6 +2828,11 @@ alert button Delete queue server test step + + Delete relay + Delete relay + No comment provided by engineer. + Delete report Delete report @@ -3026,6 +3209,11 @@ chat item action Edit chat item action + + Edit channel profile + Edit channel profile + No comment provided by engineer. + Edit group profile Edit group profile @@ -3061,6 +3249,11 @@ chat item action Enable TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Enable automatic message deletion? @@ -3206,6 +3399,11 @@ chat item action Enter Passcode No comment provided by engineer. + + Enter channel name… + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Enter correct passphrase. @@ -3231,6 +3429,11 @@ chat item action Enter password above to show! No comment provided by engineer. + + Enter relay name… + Enter relay name… + No comment provided by engineer. + Enter server manually Enter server manually @@ -3286,6 +3489,11 @@ chat item action Error adding member(s) No comment provided by engineer. + + Error adding relay + Error adding relay + alert title + Error adding server Error adding server @@ -3346,6 +3554,11 @@ chat item action Error creating address No comment provided by engineer. + + Error creating channel + Error creating channel + alert title + Error creating group Error creating group @@ -3481,11 +3694,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - Error opening group - No comment provided by engineer. - Error receiving file Error receiving file @@ -3531,6 +3739,11 @@ chat item action Error saving ICE servers No comment provided by engineer. + + Error saving channel profile + Error saving channel profile + No comment provided by engineer. + Error saving chat list Error saving chat list @@ -3676,7 +3889,8 @@ snd error text Error: %@. Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3920,7 +4134,8 @@ snd error text Fingerprint in server address does not match certificate. Fingerprint in server address does not match certificate. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3965,7 +4180,8 @@ snd error text For chat profile %@: For chat profile %@: - servers error + servers error +servers warning For console @@ -4106,6 +4322,11 @@ Error: %2$@ GIFs and stickers No comment provided by engineer. + + Get link + Get link + relay test step + Get notified when mentioned. Get notified when mentioned. @@ -4608,7 +4829,17 @@ More improvements are coming soon! Invalid name! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + Invalid relay address! + alert title + + + Invalid relay name! + Invalid relay name! + alert title Invalid response @@ -4721,6 +4952,11 @@ More improvements are coming soon! Join as %@ No comment provided by engineer. + + Join channel + Join channel + No comment provided by engineer. + Join group Join group @@ -4808,6 +5044,16 @@ This is your link for group %@! Leave swipe action + + Leave channel + Leave channel + No comment provided by engineer. + + + Leave channel? + Leave channel? + No comment provided by engineer. + Leave chat Leave chat @@ -5103,6 +5349,11 @@ This is your link for group %@! Message draft No comment provided by engineer. + + Message error + Message error + No comment provided by engineer. + Message forwarded Message forwarded @@ -5423,6 +5674,11 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + New chat relay + No comment provided by engineer. + New contact request New contact request @@ -5498,6 +5754,16 @@ This is your link for group %@! No app password Authentication unavailable + + No chat relays + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + No chat relays enabled. + servers warning + No chats No chats @@ -5648,6 +5914,11 @@ This is your link for group %@! No user identifiers. No comment provided by engineer. + + Not all relays connected + Not all relays connected + alert title + Not compatible! Not compatible! @@ -5863,6 +6134,11 @@ Requires compatible VPN. Open changes No comment provided by engineer. + + Open channel + Open channel + new chat action + Open chat Open chat @@ -5903,6 +6179,11 @@ Requires compatible VPN. Open migration to another device authentication reason + + Open new channel + Open new channel + new chat action + Open new chat Open new chat @@ -5995,6 +6276,16 @@ Requires compatible VPN. %@ alert message + + Owner + Owner + No comment provided by engineer. + + + Owners + Owners + No comment provided by engineer. + PING count PING count @@ -6204,6 +6495,16 @@ Error: %@ Preserve the last message draft, with attachments. No comment provided by engineer. + + Preset relay address + Preset relay address + No comment provided by engineer. + + + Preset relay name + Preset relay name + No comment provided by engineer. + Preset server address Preset server address @@ -6289,6 +6590,11 @@ Error: %@ Private routing timeout alert title + + Proceed + Proceed + alert action + Profile and server connections Profile and server connections @@ -6638,6 +6944,26 @@ swipe action Reject member? alert title + + Relay + Relay + No comment provided by engineer. + + + Relay address + Relay address + alert title + + + Relay connection failed + Relay connection failed + alert title + + + Relay link + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Relay server is only used if necessary. Another party can observe your IP address. @@ -6648,6 +6974,11 @@ swipe action Relay server protects your IP address, but it can observe the duration of the call. No comment provided by engineer. + + Relay test failed! + Relay test failed! + No comment provided by engineer. + Remove Remove @@ -6688,6 +7019,16 @@ swipe action Remove passphrase from keychain? No comment provided by engineer. + + Remove subscriber + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + Remove subscriber? + alert title + Removes messages and blocks members. Removes messages and blocks members. @@ -6949,6 +7290,11 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + Save (and notify subscribers) + alert button + Save admission settings? Save admission settings? @@ -6974,6 +7320,16 @@ chat item action Save and update group profile No comment provided by engineer. + + Save channel profile + Save channel profile + No comment provided by engineer. + + + Save channel profile? + Save channel profile? + alert title + Save group profile Save group profile @@ -7434,6 +7790,11 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Server requires authorization to create queues, check password. @@ -7630,6 +7991,11 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + Share relay address + No comment provided by engineer. + Share this 1-time invite link Share this 1-time invite link @@ -7815,9 +8181,9 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link - SimpleX relay link + + SimpleX relay address + SimpleX relay address simplex link type @@ -7993,6 +8359,28 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Subscription errors @@ -8093,6 +8481,11 @@ report reason Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join channel + Tap Join channel + No comment provided by engineer. + Tap Join group Tap Join group @@ -8141,13 +8534,19 @@ report reason Test failed at step %@. Test failed at step %@. - server test failure + relay test failure +server test failure Test notifications Test notifications No comment provided by engineer. + + Test relay + Test relay + No comment provided by engineer. + Test server Test server @@ -8200,6 +8599,11 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). The app will ask to confirm downloads from unknown file servers (except .onion). @@ -8390,6 +8794,16 @@ It can happen because of some bug or when the connection is compromised.This group no longer exists. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. @@ -8612,6 +9026,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Undelivered messages @@ -8864,6 +9283,11 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + Use for new channels + No comment provided by engineer. + Use for new connections Use for new connections @@ -8904,6 +9328,11 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + Use relay + No comment provided by engineer. + Use server Use server @@ -8944,6 +9373,11 @@ To connect, please ask your contact to create another connection link and check Using SimpleX Chat servers. No comment provided by engineer. + + Verify + Verify + relay test step + Verify code with desktop Verify code with desktop @@ -9064,6 +9498,16 @@ To connect, please ask your contact to create another connection link and check Voice message… No comment provided by engineer. + + Wait + Wait + alert action + + + Wait response + Wait response + relay test step + Waiting for desktop... Waiting for desktop... @@ -9366,6 +9810,11 @@ Repeat join request? You can set lock screen notification preview via settings. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. @@ -9411,6 +9860,11 @@ Repeat join request? You can't send messages! alert title + + You connected to the channel via this relay link. + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. You could not be verified; please try again. @@ -9523,6 +9977,11 @@ Repeat connection request? You will still receive calls and notifications from muted profiles when they are active. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. You will stop receiving messages from this chat. Chat history will be preserved. @@ -9568,6 +10027,11 @@ Repeat connection request? Your calls No comment provided by engineer. + + Your channel + Your channel + No comment provided by engineer. + Your chat database Your chat database @@ -9653,6 +10117,13 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Your profile **%@** will be shared. @@ -9678,6 +10149,16 @@ Repeat connection request? Your random profile No comment provided by engineer. + + Your relay address + Your relay address + No comment provided by engineer. + + + Your relay name + Your relay name + No comment provided by engineer. + Your server address Your server address @@ -9723,6 +10204,11 @@ Repeat connection request? above, then choose: No comment provided by engineer. + + accepted + accepted + No comment provided by engineer. + accepted %@ accepted %@ @@ -9743,6 +10229,11 @@ Repeat connection request? accepted you rcv group event chat item + + active + active + No comment provided by engineer. + admin admin @@ -9889,6 +10380,16 @@ marked deleted chat item preview text changing address… chat item text + + channel + channel + shown as sender role for channel messages + + + channel profile updated + channel profile updated + snd group event chat item + colored colored @@ -10035,6 +10536,11 @@ pref value deleted deleted chat item + + deleted channel + deleted channel + rcv group event chat item + deleted contact deleted contact @@ -10145,6 +10651,11 @@ pref value error No comment provided by engineer. + + error: %@ + error: %@ + receive error chat item + expired expired @@ -10275,6 +10786,11 @@ pref value left rcv group event chat item + + link + link + No comment provided by engineer. + marked deleted marked deleted @@ -10345,6 +10861,11 @@ pref value never delete after time + + new + new + No comment provided by engineer. + new message new message @@ -10468,6 +10989,11 @@ time to disappear rejected call call status + + relay + relay + member role + removed removed @@ -10478,6 +11004,11 @@ time to disappear removed %@ rcv group event chat item + + removed (%d attempts) + removed (%d attempts) + receive error chat item + removed contact address removed contact address @@ -10632,6 +11163,11 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + updated channel profile + rcv group event chat item + updated group profile updated group profile @@ -10652,6 +11188,11 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + via %@ + relay hostname + via contact address link via contact address link @@ -10727,6 +11268,11 @@ last received msg: %2$@ you are observer No comment provided by engineer. + + you are subscriber + you are subscriber + No comment provided by engineer. + you blocked %@ you blocked %@ 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 61734f2480..fe6234e16b 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -185,6 +185,10 @@ %d mes(es) time interval + + %d relays + channel relay bar + %d sec %d segundo(s) @@ -200,11 +204,37 @@ %d mensaje(s) omitido(s) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d semana(s) time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contacto(s) seleccionado(s) @@ -365,6 +399,10 @@ **Escanear / Pegar enlace**: para conectar mediante un enlace recibido. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Advertencia**: Las notificaciones automáticas instantáneas requieren una contraseña guardada en Keychain. @@ -1317,6 +1355,10 @@ swipe action ¿Bloquear miembro? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Bloqueado por administrador @@ -1367,6 +1409,10 @@ swipe action Tanto tú como tu contacto podéis enviar mensajes de voz. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Búlgaro, Finlandés, Tailandés y Ucraniano - gracias a los usuarios y [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1554,6 +1600,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Chat @@ -1639,6 +1729,22 @@ set passcode view Perfil de usuario No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Tema de chat @@ -1689,6 +1795,14 @@ set passcode view Comprobar mensajes cuando se permita. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Comprueba la dirección del servidor e inténtalo de nuevo. @@ -1834,6 +1948,10 @@ set passcode view Configure servidores ICE No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Configurar operadores de servidores @@ -1897,7 +2015,8 @@ set passcode view Connect Conectar - server test step + relay test step +server test step Connect automatically @@ -2177,7 +2296,7 @@ This is your own one-time link! Correct name to %@? ¿Corregir el nombre a %@? - No comment provided by engineer. + alert message Create @@ -2234,6 +2353,14 @@ This is your own one-time link! Crear perfil No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Crear cola @@ -2269,6 +2396,10 @@ This is your own one-time link! Creando enlace al archivo No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Creando enlace… @@ -2432,6 +2563,10 @@ This is your own one-time link! Descentralizada No comment provided by engineer. + + Decode link + relay test step + Decryption error Error descifrado @@ -2478,6 +2613,14 @@ swipe action Eliminar y notificar contacto No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Eliminar chat @@ -2649,6 +2792,10 @@ alert button Eliminar cola server test step + + Delete relay + No comment provided by engineer. + Delete report Eliminar informe @@ -3025,6 +3172,10 @@ chat item action Editar chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Editar perfil de grupo @@ -3060,6 +3211,10 @@ chat item action Activar TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? ¿Activar eliminación automática de mensajes? @@ -3205,6 +3360,10 @@ chat item action Introduce Código No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Introduce la contraseña correcta. @@ -3230,6 +3389,10 @@ chat item action ¡Introduce la contraseña arriba para mostrar! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Añadir manualmente @@ -3285,6 +3448,10 @@ chat item action Error al añadir miembro(s) No comment provided by engineer. + + Error adding relay + alert title + Error adding server Error al añadir servidor @@ -3345,6 +3512,10 @@ chat item action Error al crear dirección No comment provided by engineer. + + Error creating channel + alert title + Error creating group Error al crear grupo @@ -3480,11 +3651,6 @@ chat item action Error al abrir chat No comment provided by engineer. - - Error opening group - Error al abrir el grupo - No comment provided by engineer. - Error receiving file Error al recibir archivo @@ -3530,6 +3696,10 @@ chat item action Error al guardar servidores ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Error al guardar listas @@ -3675,7 +3845,8 @@ snd error text Error: %@. Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4090,8 @@ snd error text Fingerprint in server address does not match certificate. La huella en la dirección del servidor no coincide con el certificado. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3964,7 +4136,8 @@ snd error text For chat profile %@: Para el perfil de chat %@: - servers error + servers error +servers warning For console @@ -4105,6 +4278,10 @@ Error: %2$@ GIFs y stickers No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Las menciones ahora se notifican. @@ -4606,7 +4783,15 @@ More improvements are coming soon! Invalid name! ¡Nombre no válido! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4719,6 +4904,10 @@ More improvements are coming soon! Unirme como %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Unirme al grupo @@ -4806,6 +4995,14 @@ This is your link for group %@! Salir swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Salir del chat @@ -5101,6 +5298,10 @@ This is your link for group %@! Borrador de mensaje No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Mensaje reenviado @@ -5421,6 +5622,10 @@ This is your link for group %@! Nueva experiencia de chat 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nueva solicitud de contacto @@ -5496,6 +5701,14 @@ This is your link for group %@! Sin contraseña de la aplicación Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Sin chats @@ -5646,6 +5859,10 @@ This is your link for group %@! Sin identificadores de usuario. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! ¡No compatible! @@ -5861,6 +6078,10 @@ Requiere activación de la VPN. Abrir cambios No comment provided by engineer. + + Open channel + new chat action + Open chat Abrir chat @@ -5901,6 +6122,10 @@ Requiere activación de la VPN. Abrir menú migración a otro dispositivo authentication reason + + Open new channel + new chat action + Open new chat Abrir chat nuevo @@ -5993,6 +6218,14 @@ Requiere activación de la VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count Contador PING @@ -6202,6 +6435,14 @@ Error: %@ Conserva el último borrador del mensaje con los datos adjuntos. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Dirección predefinida del servidor @@ -6287,6 +6528,10 @@ Error: %@ Timeout enrutamiento privado alert title + + Proceed + alert action + Profile and server connections Eliminar perfil y conexiones @@ -6636,6 +6881,22 @@ swipe action ¿Rechazar al miembro? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP. @@ -6646,6 +6907,10 @@ swipe action El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Eliminar @@ -6686,6 +6951,14 @@ swipe action ¿Eliminar contraseña de Keychain? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Elimina mensajes y bloquea miembros. @@ -6947,6 +7220,10 @@ chat item action Guardar (y notificar miembros) alert button + + Save (and notify subscribers) + alert button + Save admission settings? ¿Guardar configuración? @@ -6972,6 +7249,14 @@ chat item action Guardar y actualizar perfil del grupo No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Guardar perfil de grupo @@ -7432,6 +7717,10 @@ chat item action El protocolo del servidor ha cambiado. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. El servidor requiere autorización para crear colas, comprueba la contraseña. @@ -7628,6 +7917,10 @@ chat item action Perfil a compartir No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Comparte este enlace de un solo uso @@ -7813,9 +8106,8 @@ chat item action Protocolos de SimpleX auditados por Trail of Bits. No comment provided by engineer. - - SimpleX relay link - Enlace de servidor SimpleX + + SimpleX relay address simplex link type @@ -7991,6 +8283,23 @@ report reason Suscritas No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Errores de suscripción @@ -8091,6 +8400,10 @@ report reason Pulsa Crear dirección SimpleX en el menú para crearla más tarde. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Pulsa Unirme al grupo @@ -8139,13 +8452,18 @@ report reason Test failed at step %@. Prueba no superada en el paso %@. - server test failure + relay test failure +server test failure Test notifications Probar notificaciones No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Probar servidor @@ -8198,6 +8516,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion). @@ -8388,6 +8710,14 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Este grupo ya no existe. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible. @@ -8610,6 +8940,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.¿Desbloquear miembro? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Mensajes no entregados @@ -8862,6 +9196,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Uso para mensajes No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Para conexiones nuevas @@ -8902,6 +9240,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar enrutamiento privado con servidores de mensaje desconocidos. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Usar servidor @@ -8942,6 +9284,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar servidores SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Verificar código con ordenador @@ -9062,6 +9408,14 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Mensaje de voz… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Esperando ordenador... @@ -9364,6 +9718,10 @@ Repeat join request? Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Puedes compartir el enlace o el código QR para que cualquiera pueda unirse al grupo. Si más tarde lo eliminas, no afectará a los miembros del grupo. @@ -9409,6 +9767,10 @@ Repeat join request? ¡No puedes enviar mensajes! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. No has podido ser autenticado. Inténtalo de nuevo. @@ -9521,6 +9883,10 @@ Repeat connection request? Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Dejarás de recibir mensajes del chat. El historial del chat se conserva. @@ -9566,6 +9932,10 @@ Repeat connection request? Llamadas No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Base de datos @@ -9651,6 +10021,11 @@ Repeat connection request? Tu perfil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. El perfil **%@** será compartido. @@ -9676,6 +10051,14 @@ Repeat connection request? Tu perfil aleatorio No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Dirección del servidor @@ -9721,6 +10104,10 @@ Repeat connection request? y después elige: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ %@ aceptado @@ -9741,6 +10128,10 @@ Repeat connection request? te ha admitido rcv group event chat item + + active + No comment provided by engineer. + admin administrador @@ -9887,6 +10278,14 @@ marked deleted chat item preview text cambiando de servidor… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored coloreado @@ -10033,6 +10432,10 @@ pref value eliminado deleted chat item + + deleted channel + rcv group event chat item + deleted contact contacto eliminado @@ -10143,6 +10546,10 @@ pref value error No comment provided by engineer. + + error: %@ + receive error chat item + expired expirados @@ -10272,6 +10679,10 @@ pref value ha salido rcv group event chat item + + link + No comment provided by engineer. + marked deleted marcado eliminado @@ -10342,6 +10753,10 @@ pref value nunca delete after time + + new + No comment provided by engineer. + new message mensaje nuevo @@ -10465,6 +10880,10 @@ time to disappear llamada rechazada call status + + relay + member role + removed expulsado @@ -10475,6 +10894,10 @@ time to disappear ha expulsado a %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address dirección de contacto eliminada @@ -10629,6 +11052,10 @@ last received msg: %2$@ desprotegida No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile ha actualizado el perfil del grupo @@ -10649,6 +11076,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link mediante enlace de dirección de contacto @@ -10724,6 +11155,10 @@ last received msg: %2$@ Tu rol es observador No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ has bloqueado a %@ 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 5a7813dfe5..89bea61cee 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -172,6 +172,10 @@ %d kuukautta time interval + + %d relays + channel relay bar + %d sec %d sek @@ -186,11 +190,37 @@ %d ohitettua viestiä integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d viikkoa time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -201,6 +231,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld kontaktia valittu @@ -340,6 +374,10 @@ **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin. @@ -1195,6 +1233,10 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin No comment provided by engineer. @@ -1240,6 +1282,10 @@ swipe action Sekä sinä että kontaktisi voitte lähettää ääniviestejä. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. @@ -1407,6 +1453,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat No comment provided by engineer. @@ -1483,6 +1573,22 @@ set passcode view Käyttäjäprofiili No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme No comment provided by engineer. @@ -1524,6 +1630,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Tarkista palvelimen osoite ja yritä uudelleen. @@ -1651,6 +1765,10 @@ set passcode view Määritä ICE-palvelimet No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators No comment provided by engineer. @@ -1707,7 +1825,8 @@ set passcode view Connect Yhdistä - server test step + relay test step +server test step Connect automatically @@ -1950,7 +2069,7 @@ This is your own one-time link! Correct name to %@? - No comment provided by engineer. + alert message Create @@ -2003,6 +2122,14 @@ This is your own one-time link! Luo profiilisi No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Luo jono @@ -2033,6 +2160,10 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2189,6 +2320,10 @@ This is your own one-time link! Hajautettu No comment provided by engineer. + + Decode link + relay test step + Decryption error Salauksen purkuvirhe @@ -2232,6 +2367,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2393,6 +2536,10 @@ alert button Poista jono server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2730,6 +2877,10 @@ chat item action Muokkaa chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Muokkaa ryhmäprofiilia @@ -2763,6 +2914,10 @@ chat item action Ota TCP-säilytys käyttöön No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Ota automaattinen viestien poisto käyttöön? @@ -2898,6 +3053,10 @@ chat item action Syötä pääsykoodi No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Anna oikea tunnuslause. @@ -2921,6 +3080,10 @@ chat item action Kirjoita yllä oleva salasana näyttääksesi! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Syötä palvelin manuaalisesti @@ -2972,6 +3135,10 @@ chat item action Virhe lisättäessä jäseniä No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3024,6 +3191,10 @@ chat item action Virhe osoitteen luomisessa No comment provided by engineer. + + Error creating channel + alert title + Error creating group Virhe ryhmän luomisessa @@ -3149,10 +3320,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Virhe tiedoston vastaanottamisessa @@ -3192,6 +3359,10 @@ chat item action Virhe ICE-palvelimien tallentamisessa No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3325,7 +3496,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3542,7 +3714,8 @@ snd error text Fingerprint in server address does not match certificate. Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3584,7 +3757,8 @@ snd error text For chat profile %@: - servers error + servers error +servers warning For console @@ -3703,6 +3877,10 @@ Error: %2$@ GIFit ja tarrat No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. @@ -4166,7 +4344,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4275,6 +4461,10 @@ More improvements are coming soon! Liity %@:nä No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Liity ryhmään @@ -4354,6 +4544,14 @@ This is your link for group %@! Poistu swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4622,6 +4820,10 @@ This is your link for group %@! Viestiluonnos No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -4902,6 +5104,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Uusi kontaktipyyntö @@ -4971,6 +5177,14 @@ This is your link for group %@! Ei sovelluksen salasanaa Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5102,6 +5316,10 @@ This is your link for group %@! Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5301,6 +5519,10 @@ Edellyttää VPN:n sallimista. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat Avaa keskustelu @@ -5335,6 +5557,10 @@ Edellyttää VPN:n sallimista. Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5408,6 +5634,14 @@ Edellyttää VPN:n sallimista. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING-määrä @@ -5599,6 +5833,14 @@ Error: %@ Säilytä viimeinen viestiluonnos liitteineen. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Esiasetettu palvelimen osoite @@ -5672,6 +5914,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profiili- ja palvelinyhteydet @@ -5991,6 +6237,22 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Välityspalvelinta käytetään vain tarvittaessa. Toinen osapuoli voi tarkkailla IP-osoitettasi. @@ -6001,6 +6263,10 @@ swipe action Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Poista @@ -6037,6 +6303,14 @@ swipe action Poista tunnuslause avainnipusta? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6268,6 +6542,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6291,6 +6569,14 @@ chat item action Tallenna ja päivitä ryhmäprofiili No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Tallenna ryhmäprofiili @@ -6705,6 +6991,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana @@ -6880,6 +7170,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -7046,8 +7340,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7205,6 +7499,23 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7293,6 +7604,10 @@ report reason Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group No comment provided by engineer. @@ -7336,12 +7651,17 @@ report reason Test failed at step %@. Testi epäonnistui vaiheessa %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Testipalvelin @@ -7392,6 +7712,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7564,6 +7888,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Tätä ryhmää ei enää ole olemassa. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7757,6 +8089,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -7984,6 +8320,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Käytä uusiin yhteyksiin @@ -8019,6 +8359,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Käytä palvelinta @@ -8053,6 +8397,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Käyttää SimpleX Chat -palvelimia. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8162,6 +8510,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Ääniviesti… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... No comment provided by engineer. @@ -8428,6 +8784,10 @@ Repeat join request? Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Voit jakaa linkin tai QR-koodin - kuka tahansa voi liittyä ryhmään. Et menetä ryhmän jäseniä, jos poistat sen myöhemmin. @@ -8470,6 +8830,10 @@ Repeat join request? Et voi lähettää viestejä! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Sinua ei voitu todentaa; yritä uudelleen. @@ -8574,6 +8938,10 @@ Repeat connection request? Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8617,6 +8985,10 @@ Repeat connection request? Puhelusi No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Keskustelut-tietokantasi @@ -8695,6 +9067,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Profiilisi **%@** jaetaan. @@ -8719,6 +9096,14 @@ Repeat connection request? Satunnainen profiilisi No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Palvelimesi osoite @@ -8763,6 +9148,10 @@ Repeat connection request? edellä, valitse sitten: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8780,6 +9169,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin ylläpitäjä @@ -8914,6 +9307,14 @@ marked deleted chat item preview text muuttamassa osoitetta… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored värillinen @@ -9054,6 +9455,10 @@ pref value poistettu deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9162,6 +9567,10 @@ pref value virhe No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9285,6 +9694,10 @@ pref value poistunut rcv group event chat item + + link + No comment provided by engineer. + marked deleted merkitty poistetuksi @@ -9351,6 +9764,10 @@ pref value ei koskaan delete after time + + new + No comment provided by engineer. + new message uusi viesti @@ -9464,6 +9881,10 @@ time to disappear hylätty puhelu call status + + relay + member role + removed poistettu @@ -9474,6 +9895,10 @@ time to disappear %@ poistettu rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address profile update event chat item @@ -9605,6 +10030,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile päivitetty ryhmäprofiili @@ -9623,6 +10052,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link kontaktiosoitelinkillä @@ -9694,6 +10127,10 @@ last received msg: %2$@ olet tarkkailija No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item 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 7e386fe50c..5fcada4ccf 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -185,6 +185,10 @@ %d mois time interval + + %d relays + channel relay bar + %d sec %d sec @@ -200,11 +204,37 @@ %d message·s sauté·s integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d semaines time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contact·s sélectionné·s @@ -365,6 +399,10 @@ **Scanner / Coller** : pour vous connecter via un lien que vous avez reçu. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Avertissement** : les notifications push instantanées nécessitent une phrase secrète enregistrée dans la keychain. @@ -1312,6 +1350,10 @@ swipe action Bloquer ce membre ? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Bloqué par l'administrateur @@ -1360,6 +1402,10 @@ swipe action Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) ! @@ -1545,6 +1591,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Discussions @@ -1630,6 +1720,22 @@ set passcode view Profil d'utilisateur No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Thème de chat @@ -1676,6 +1782,14 @@ set passcode view Consulter les messages quand c'est possible. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Vérifiez l'adresse du serveur et réessayez. @@ -1821,6 +1935,10 @@ set passcode view Configurer les serveurs ICE No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Configurer les opérateurs de serveur @@ -1884,7 +2002,8 @@ set passcode view Connect Se connecter - server test step + relay test step +server test step Connect automatically @@ -2162,7 +2281,7 @@ Il s'agit de votre propre lien unique ! Correct name to %@? Corriger le nom pour %@ ? - No comment provided by engineer. + alert message Create @@ -2219,6 +2338,14 @@ Il s'agit de votre propre lien unique ! Créer le profil No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Créer une file d'attente @@ -2253,6 +2380,10 @@ Il s'agit de votre propre lien unique ! Création d'un lien d'archive No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Création d'un lien… @@ -2416,6 +2547,10 @@ Il s'agit de votre propre lien unique ! Décentralisé No comment provided by engineer. + + Decode link + relay test step + Decryption error Erreur de déchiffrement @@ -2462,6 +2597,14 @@ swipe action Supprimer et en informer le contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Supprimer la discussion @@ -2630,6 +2773,10 @@ alert button Supprimer la file d'attente server test step + + Delete relay + No comment provided by engineer. + Delete report Supprimer le rapport @@ -3004,6 +3151,10 @@ chat item action Modifier chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Modifier le profil du groupe @@ -3038,6 +3189,10 @@ chat item action Activer le TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Activer la suppression automatique des messages ? @@ -3182,6 +3337,10 @@ chat item action Entrer le code d'accès No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Entrez la phrase secrète correcte. @@ -3207,6 +3366,10 @@ chat item action Entrez ci-dessus le mot de passe pour afficher le profil ! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Entrer un serveur manuellement @@ -3261,6 +3424,10 @@ chat item action Erreur lors de l'ajout de membre·s No comment provided by engineer. + + Error adding relay + alert title + Error adding server Erreur lors de l'ajout du serveur @@ -3318,6 +3485,10 @@ chat item action Erreur lors de la création de l'adresse No comment provided by engineer. + + Error creating channel + alert title + Error creating group Erreur lors de la création du groupe @@ -3452,10 +3623,6 @@ chat item action Erreur lors de l'ouverture du chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Erreur lors de la réception du fichier @@ -3500,6 +3667,10 @@ chat item action Erreur lors de la sauvegarde des serveurs ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Erreur lors de l'enregistrement de la liste des chats @@ -3643,7 +3814,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3883,7 +4055,8 @@ snd error text Fingerprint in server address does not match certificate. Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3926,7 +4099,8 @@ snd error text For chat profile %@: Pour le profil de discussion %@ : - servers error + servers error +servers warning For console @@ -4066,6 +4240,10 @@ Erreur : %2$@ GIFs et stickers No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. @@ -4554,7 +4732,15 @@ D'autres améliorations sont à venir ! Invalid name! Nom invalide ! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4666,6 +4852,10 @@ D'autres améliorations sont à venir ! rejoindre entant que %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Rejoindre le groupe @@ -4752,6 +4942,14 @@ Voici votre lien pour le groupe %@ ! Quitter swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Quitter la discussion @@ -5033,6 +5231,10 @@ Voici votre lien pour le groupe %@ ! Brouillon de message No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Message transféré @@ -5347,6 +5549,10 @@ Voici votre lien pour le groupe %@ ! Nouvelle expérience de discussion 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nouvelle demande de contact @@ -5420,6 +5626,14 @@ Voici votre lien pour le groupe %@ ! Pas de mot de passe pour l'app Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5562,6 +5776,10 @@ Voici votre lien pour le groupe %@ ! Aucun identifiant d'utilisateur. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Non compatible ! @@ -5770,6 +5988,10 @@ Nécessite l'activation d'un VPN. Ouvrir les modifications No comment provided by engineer. + + Open channel + new chat action + Open chat Ouvrir le chat @@ -5807,6 +6029,10 @@ Nécessite l'activation d'un VPN. Ouvrir le transfert vers un autre appareil authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5892,6 +6118,14 @@ Nécessite l'activation d'un VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count Nombre de PING @@ -6097,6 +6331,14 @@ Erreur : %@ Conserver le brouillon du dernier message, avec les pièces jointes. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Adresse du serveur prédéfinie @@ -6178,6 +6420,10 @@ Erreur : %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profil et connexions au serveur @@ -6521,6 +6767,22 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Le serveur relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP. @@ -6531,6 +6793,10 @@ swipe action Le serveur relais protège votre adresse IP, mais il peut observer la durée de l'appel. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Supprimer @@ -6569,6 +6835,14 @@ swipe action Supprimer la phrase secrète de la keychain ? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6814,6 +7088,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6838,6 +7116,14 @@ chat item action Enregistrer et mettre à jour le profil du groupe No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Enregistrer le profil du groupe @@ -7286,6 +7572,10 @@ chat item action Le protocole du serveur a été modifié. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe @@ -7476,6 +7766,10 @@ chat item action Partager le profil No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Partagez ce lien d'invitation unique @@ -7656,8 +7950,8 @@ chat item action Protocoles SimpleX audité par Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7831,6 +8125,23 @@ report reason Inscriptions No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Erreurs d'inscription @@ -7926,6 +8237,10 @@ report reason Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group No comment provided by engineer. @@ -7973,12 +8288,17 @@ report reason Test failed at step %@. Échec du test à l'étape %@. - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Tester le serveur @@ -8030,6 +8350,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. L'application protège votre vie privée en utilisant des opérateurs différents pour chaque conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). L'application demandera de confirmer les téléchargements à partir de serveurs de fichiers inconnus (sauf .onion). @@ -8218,6 +8542,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Ce groupe n'existe plus. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8432,6 +8764,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Débloquer ce membre ? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Messages non distribués @@ -8674,6 +9010,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser pour les messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Utiliser pour les nouvelles connexions @@ -8713,6 +9053,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser le routage privé avec des serveurs inconnus. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Utiliser ce serveur @@ -8752,6 +9096,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Vous utilisez les serveurs SimpleX. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Vérifier le code avec le bureau @@ -8871,6 +9219,14 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Message vocal… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... En attente du bureau... @@ -9170,6 +9526,10 @@ Répéter la demande d'adhésion ? Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Vous pouvez partager un lien ou un code QR - n'importe qui pourra rejoindre le groupe. Vous ne perdrez pas les membres du groupe si vous le supprimez par la suite. @@ -9214,6 +9574,10 @@ Répéter la demande d'adhésion ? Vous ne pouvez pas envoyer de messages ! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Vous n'avez pas pu être vérifié·e ; veuillez réessayer. @@ -9324,6 +9688,10 @@ Répéter la demande de connexion ? Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu'ils sont actifs. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Vous ne recevrez plus de messages de cette discussion. L'historique sera préservé. @@ -9368,6 +9736,10 @@ Répéter la demande de connexion ? Vos appels No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Votre base de données de chat @@ -9450,6 +9822,11 @@ Répéter la demande de connexion ? Votre profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Votre profil **%@** sera partagé. @@ -9475,6 +9852,14 @@ Répéter la demande de connexion ? Votre profil aléatoire No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Votre adresse de serveur @@ -9520,6 +9905,10 @@ Répéter la demande de connexion ? ci-dessus, puis choisissez : No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -9538,6 +9927,10 @@ Répéter la demande de connexion ? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin admin @@ -9681,6 +10074,14 @@ marked deleted chat item preview text changement d'adresse… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored coloré @@ -9823,6 +10224,10 @@ pref value supprimé deleted chat item + + deleted channel + rcv group event chat item + deleted contact contact supprimé @@ -9933,6 +10338,10 @@ pref value erreur No comment provided by engineer. + + error: %@ + receive error chat item + expired expiré @@ -10060,6 +10469,10 @@ pref value a quitté rcv group event chat item + + link + No comment provided by engineer. + marked deleted supprimé @@ -10128,6 +10541,10 @@ pref value jamais delete after time + + new + No comment provided by engineer. + new message nouveau message @@ -10245,6 +10662,10 @@ time to disappear appel rejeté call status + + relay + member role + removed supprimé @@ -10255,6 +10676,10 @@ time to disappear a retiré %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address suppression de l'adresse de contact @@ -10402,6 +10827,10 @@ dernier message reçu : %2$@ non protégé No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile mise à jour du profil de groupe @@ -10422,6 +10851,10 @@ dernier message reçu : %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link via le lien d'adresse du contact @@ -10496,6 +10929,10 @@ dernier message reçu : %2$@ vous êtes observateur No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ vous avez bloqué %@ 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 0ed5dc19ea..4837804697 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -185,6 +185,10 @@ %d hónap time interval + + %d relays + channel relay bar + %d sec %d mp @@ -200,11 +204,37 @@ %d üzenet kihagyva integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d hét time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld partner kiválasztva @@ -365,6 +399,10 @@ **Hivatkozás beolvasása / beillesztése**: egy kapott hivatkozáson keresztüli kapcsolódáshoz. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Figyelmeztetés:** Az azonnali leküldéses értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges. @@ -844,7 +882,7 @@ swipe action All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra. + Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-átjátszókra. No comment provided by engineer. @@ -994,7 +1032,7 @@ swipe action Always use relay - Mindig legyen használva továbbítókiszolgáló + Mindig legyen használva átjátszó No comment provided by engineer. @@ -1317,6 +1355,10 @@ swipe action Letiltja a tagot? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Letiltva az adminisztrátor által @@ -1367,6 +1409,10 @@ swipe action Mindkét fél küldhet hangüzeneteket. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bolgár, finn, thai és ukrán – köszönet a felhasználóknak és a [Weblate-nek](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1554,6 +1600,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Csevegés @@ -1639,6 +1729,22 @@ set passcode view Csevegési profil No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Csevegés témája @@ -1689,6 +1795,14 @@ set passcode view Üzenetek ellenőrzése, amikor engedélyezett. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Kiszolgáló címének ellenőrzése és újrapróbálkozás. @@ -1834,6 +1948,10 @@ set passcode view ICE-kiszolgálók beállítása No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Kiszolgálóüzemeltetők beállítása @@ -1897,7 +2015,8 @@ set passcode view Connect Kapcsolódás - server test step + relay test step +server test step Connect automatically @@ -2025,6 +2144,7 @@ Ez a saját egyszer használható meghívója! Connection failed + Nem sikerült létrehozni a kapcsolatot No comment provided by engineer. @@ -2177,7 +2297,7 @@ Ez a saját egyszer használható meghívója! Correct name to %@? Helyesbíti a nevet a következőre: %@? - No comment provided by engineer. + alert message Create @@ -2234,6 +2354,14 @@ Ez a saját egyszer használható meghívója! Profil létrehozása No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Várólista létrehozása @@ -2269,6 +2397,10 @@ Ez a saját egyszer használható meghívója! Archívum hivatkozás létrehozása No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Hivatkozás létrehozása… @@ -2432,6 +2564,10 @@ Ez a saját egyszer használható meghívója! Decentralizált No comment provided by engineer. + + Decode link + relay test step + Decryption error Titkosítás-visszafejtési hiba @@ -2478,6 +2614,14 @@ swipe action Törlés, és a partner értesítése No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Csevegés törlése @@ -2649,6 +2793,10 @@ alert button Várólista törlése server test step + + Delete relay + No comment provided by engineer. + Delete report Jelentés törlése @@ -2741,7 +2889,7 @@ alert button Destination server address of %@ is incompatible with forwarding server %@ settings. - A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbítókiszolgáló beállításaival. + A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival. No comment provided by engineer. @@ -2751,7 +2899,7 @@ alert button Destination server version of %@ is incompatible with forwarding server %@. - A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbítókiszolgálóval. + A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval. No comment provided by engineer. @@ -3025,6 +3173,10 @@ chat item action Szerkesztés chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Csoportprofil szerkesztése @@ -3060,6 +3212,10 @@ chat item action TCP életben tartása No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Engedélyezi az automatikus üzenettörlést? @@ -3205,6 +3361,10 @@ chat item action Adja meg a jelkódot No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Adja meg a helyes jelmondatot. @@ -3230,6 +3390,10 @@ chat item action Adja meg a jelszót fentebb a megjelenítéshez! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Kiszolgáló megadása kézzel @@ -3285,6 +3449,10 @@ chat item action Hiba történt a tag(ok) hozzáadásakor No comment provided by engineer. + + Error adding relay + alert title + Error adding server Hiba történt a kiszolgáló hozzáadásakor @@ -3332,7 +3500,7 @@ chat item action Error connecting to forwarding server %@. Please try later. - Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + Hiba történt a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. alert message @@ -3345,6 +3513,10 @@ chat item action Hiba történt a cím létrehozásakor No comment provided by engineer. + + Error creating channel + alert title + Error creating group Hiba történt a csoport létrehozásakor @@ -3480,11 +3652,6 @@ chat item action Hiba történt a csevegés megnyitásakor No comment provided by engineer. - - Error opening group - Hiba történt a csoport megnyitásakor - No comment provided by engineer. - Error receiving file Hiba történt a fájl fogadásakor @@ -3530,6 +3697,10 @@ chat item action Hiba történt az ICE-kiszolgálók mentésekor No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Hiba történt a csevegési lista mentésekor @@ -3675,7 +3846,8 @@ snd error text Error: %@. Hiba: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3913,13 +4085,14 @@ snd error text Fingerprint in forwarding server address does not match certificate: %@. - A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@. + A továbbító kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@. No comment provided by engineer. Fingerprint in server address does not match certificate. A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3964,7 +4137,8 @@ snd error text For chat profile %@: A(z) %@ nevű csevegési profilhoz: - servers error + servers error +servers warning For console @@ -4038,30 +4212,30 @@ snd error text Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. - A(z) %1$@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később. + A(z) %1$@ továbbító kiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később. alert message Forwarding server address is incompatible with network settings: %@. - A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. + A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. Forwarding server version is incompatible with network settings: %@. - A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@. + A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. Forwarding server: %1$@ Destination server error: %2$@ - Továbbítókiszolgáló: %1$@ + Továbbító kiszolgáló: %1$@ Célkiszolgáló-hiba: %2$@ snd error text Forwarding server: %1$@ Error: %2$@ - Továbbítókiszolgáló: %1$@ + Továbbító kiszolgáló: %1$@ Hiba: %2$@ snd error text @@ -4105,6 +4279,10 @@ Hiba: %2$@ GIF-ek és matricák No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Kapjon értesítést, ha megemlítik. @@ -4347,6 +4525,7 @@ Hiba: %2$@ If you joined or created channels, they will stop working permanently. + Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni. down migration warning @@ -4606,7 +4785,15 @@ További fejlesztések hamarosan! Invalid name! Érvénytelen név! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4719,6 +4906,10 @@ További fejlesztések hamarosan! Csatlakozás mint: %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Csatlakozás a csoporthoz @@ -4806,6 +4997,14 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Elhagyás swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Csevegés elhagyása @@ -5101,6 +5300,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Piszkozatok No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Továbbított üzenet @@ -5421,6 +5624,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Új csevegési élmény 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Új partneri kapcsolatkérés @@ -5496,6 +5703,14 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Nincs alkalmazás jelszó Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Nincsenek csevegések @@ -5646,6 +5861,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Nincsenek felhasználói azonosítók. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Nem kompatibilis! @@ -5861,6 +6080,10 @@ VPN engedélyezése szükséges. Módosítások megtekintése No comment provided by engineer. + + Open channel + new chat action + Open chat Csevegés megnyitása @@ -5901,6 +6124,10 @@ VPN engedélyezése szükséges. Átköltöztetés indítása egy másik eszközre authentication reason + + Open new channel + new chat action + Open new chat Új csevegés megnyitása @@ -5993,6 +6220,14 @@ VPN engedélyezése szükséges. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING-ek száma @@ -6202,9 +6437,17 @@ Hiba: %@ Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address - Az előre beállított kiszolgáló címe + Előre beállított kiszolgáló címe No comment provided by engineer. @@ -6287,6 +6530,10 @@ Hiba: %@ Privát útválasztás időtúllépése alert title + + Proceed + alert action + Profile and server connections Profil és kiszolgálókapcsolatok @@ -6380,7 +6627,7 @@ Hiba: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. - Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben. + Védje az IP-címét a partnerei által kiválasztott üzenetváltási átjátszókkal szemben. Engedélyezze a *Hálózat és kiszolgálók* menüben. No comment provided by engineer. @@ -6636,14 +6883,34 @@ swipe action Elutasítja a tagot? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. - A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. + Az átjátszó csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. No comment provided by engineer. Relay server protects your IP address, but it can observe the duration of the call. - A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. + Az átjátszó megvédi az IP-címét, de megfigyelheti a hívás időtartamát. + No comment provided by engineer. + + + Relay test failed! No comment provided by engineer. @@ -6686,6 +6953,14 @@ swipe action Eltávolítja a jelmondatot a kulcstartóból? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Üzenetek eltávolítása és a tagok tiltása. @@ -6947,6 +7222,10 @@ chat item action Mentés (és a tagok értesítése) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Menti a befogadási beállításokat? @@ -6972,6 +7251,14 @@ chat item action Mentés és a csoportprofil frissítése No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Csoportprofil mentése @@ -7432,6 +7719,10 @@ chat item action A kiszolgálóprotokoll módosult. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát. @@ -7628,6 +7919,10 @@ chat item action Profil megosztása No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Ennek az egyszer használható meghívónak a megosztása @@ -7813,9 +8108,8 @@ chat item action A SimpleX protokollokat a Trail of Bits auditálta. No comment provided by engineer. - - SimpleX relay link - SimpleX továbbítókiszolgáló-hivatkozás + + SimpleX relay address simplex link type @@ -7991,6 +8285,23 @@ report reason Feliratkozva No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Feliratkozási hibák @@ -8091,6 +8402,10 @@ report reason Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Koppintson a „Csatlakozás a csoporthoz” gombra @@ -8139,13 +8454,18 @@ report reason Test failed at step %@. A teszt a(z) %@ lépésnél sikertelen volt. - server test failure + relay test failure +server test failure Test notifications Értesítések tesztelése No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Kiszolgáló tesztelése @@ -8198,6 +8518,10 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését. @@ -8388,6 +8712,14 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Ez a csoport már nem létezik. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől. @@ -8610,6 +8942,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Feloldja a tag letiltását? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Kézbesítetlen üzenetek @@ -8862,6 +9198,10 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Használat az üzenetekhez No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Használat új kapcsolatokhoz @@ -8902,6 +9242,10 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Privát útválasztás használata az ismeretlen kiszolgálókhoz. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Kiszolgáló használata @@ -8942,6 +9286,10 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso SimpleX Chat kiszolgálók használatban. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Kód ellenőrzése a számítógépen @@ -9062,6 +9410,14 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Hangüzenet… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Várakozás a számítógép-alkalmazásra… @@ -9189,7 +9545,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. - Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-átjátszók számára: %@. alert message @@ -9364,6 +9720,10 @@ Megismétli a csatlakozási kérést? A lezárási képernyő értesítési előnézetét az „Értesítések” menüben állíthatja be. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoporthivatkozást később törli, akkor nem fogja elveszíteni a csoport meglévő tagjait. @@ -9409,6 +9769,10 @@ Megismétli a csatlakozási kérést? Ön nem tud üzeneteket küldeni! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Nem sikerült ellenőrizni; próbálja meg újra. @@ -9521,6 +9885,10 @@ Megismétli a kapcsolódási kérést? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. @@ -9566,6 +9934,10 @@ Megismétli a kapcsolódási kérést? Hívások No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Csevegési adatbázis @@ -9651,6 +10023,11 @@ Megismétli a kapcsolódási kérést? Saját profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. A(z) **%@** nevű profilja meg lesz osztva. @@ -9676,6 +10053,14 @@ Megismétli a kapcsolódási kérést? Véletlenszerű profil No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Saját SMP-kiszolgálójának címe @@ -9721,6 +10106,10 @@ Megismétli a kapcsolódási kérést? gombra fent, majd válassza ki: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ befogadta őt: %@ @@ -9741,6 +10130,10 @@ Megismétli a kapcsolódási kérést? befogadta Önt rcv group event chat item + + active + No comment provided by engineer. + admin adminisztrátor @@ -9887,6 +10280,14 @@ marked deleted chat item preview text cím módosítása… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored színezett @@ -10033,6 +10434,10 @@ pref value törölve deleted chat item + + deleted channel + rcv group event chat item + deleted contact törölt partner @@ -10143,6 +10548,10 @@ pref value hiba No comment provided by engineer. + + error: %@ + receive error chat item + expired lejárt @@ -10150,6 +10559,7 @@ pref value failed + sikertelen No comment provided by engineer. @@ -10272,6 +10682,10 @@ pref value elhagyta a csoportot rcv group event chat item + + link + No comment provided by engineer. + marked deleted törlésre jelölve @@ -10342,6 +10756,10 @@ pref value soha delete after time + + new + No comment provided by engineer. + new message új üzenet @@ -10465,6 +10883,10 @@ time to disappear elutasított hívás call status + + relay + member role + removed eltávolítva @@ -10475,6 +10897,10 @@ time to disappear eltávolította őt: %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address eltávolította a kapcsolattartási címet @@ -10629,6 +11055,10 @@ utoljára fogadott üzenet: %2$@ nem védett No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile frissítette a csoportprofilt @@ -10649,6 +11079,10 @@ utoljára fogadott üzenet: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link a kapcsolattartási címhivatkozáson keresztül @@ -10666,7 +11100,7 @@ utoljára fogadott üzenet: %2$@ via relay - továbbítókiszolgálón keresztül + átjátszón keresztül No comment provided by engineer. @@ -10724,6 +11158,10 @@ utoljára fogadott üzenet: %2$@ Ön megfigyelő No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ Ön letiltotta őt: %@ 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 97061054e8..2d0566afc6 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -185,6 +185,10 @@ %d mesi time interval + + %d relays + channel relay bar + %d sec %d sec @@ -200,11 +204,37 @@ %d messaggio/i saltato/i integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d settimane time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contatto/i selezionato/i @@ -365,6 +399,10 @@ **Scansiona / Incolla link**: per connetterti tramite un link che hai ricevuto. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi. @@ -1317,6 +1355,10 @@ swipe action Bloccare il membro? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Bloccato dall'amministratore @@ -1367,6 +1409,10 @@ swipe action Sia tu che il tuo contatto potete inviare messaggi vocali. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1554,6 +1600,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Chat @@ -1639,6 +1729,22 @@ set passcode view Profilo utente No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Tema della chat @@ -1689,6 +1795,14 @@ set passcode view Controlla i messaggi quando consentito. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Controlla l'indirizzo del server e riprova. @@ -1834,6 +1948,10 @@ set passcode view Configura server ICE No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Configura gli operatori dei server @@ -1897,7 +2015,8 @@ set passcode view Connect Connetti - server test step + relay test step +server test step Connect automatically @@ -2025,6 +2144,7 @@ Questo è il tuo link una tantum! Connection failed + Connessione fallita No comment provided by engineer. @@ -2177,7 +2297,7 @@ Questo è il tuo link una tantum! Correct name to %@? Correggere il nome a %@? - No comment provided by engineer. + alert message Create @@ -2234,6 +2354,14 @@ Questo è il tuo link una tantum! Crea profilo No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Crea coda @@ -2269,6 +2397,10 @@ Questo è il tuo link una tantum! Creazione link dell'archivio No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Creazione link… @@ -2432,6 +2564,10 @@ Questo è il tuo link una tantum! Decentralizzato No comment provided by engineer. + + Decode link + relay test step + Decryption error Errore di decifrazione @@ -2478,6 +2614,14 @@ swipe action Elimina e avvisa il contatto No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Elimina chat @@ -2649,6 +2793,10 @@ alert button Elimina coda server test step + + Delete relay + No comment provided by engineer. + Delete report Elimina la segnalazione @@ -3025,6 +3173,10 @@ chat item action Modifica chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Modifica il profilo del gruppo @@ -3060,6 +3212,10 @@ chat item action Attiva il keep-alive TCP No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Attivare l'eliminazione automatica dei messaggi? @@ -3205,6 +3361,10 @@ chat item action Inserisci il codice di accesso No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Inserisci la password giusta. @@ -3230,6 +3390,10 @@ chat item action Inserisci la password sopra per mostrare! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Inserisci il server a mano @@ -3285,6 +3449,10 @@ chat item action Errore di aggiunta membro/i No comment provided by engineer. + + Error adding relay + alert title + Error adding server Errore di aggiunta del server @@ -3345,6 +3513,10 @@ chat item action Errore nella creazione dell'indirizzo No comment provided by engineer. + + Error creating channel + alert title + Error creating group Errore nella creazione del gruppo @@ -3480,11 +3652,6 @@ chat item action Errore di apertura della chat No comment provided by engineer. - - Error opening group - Errore di preparazione del gruppo - No comment provided by engineer. - Error receiving file Errore nella ricezione del file @@ -3530,6 +3697,10 @@ chat item action Errore nel salvataggio dei server ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Errore nel salvataggio dell'elenco di chat @@ -3675,7 +3846,8 @@ snd error text Error: %@. Errore: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4091,8 @@ snd error text Fingerprint in server address does not match certificate. L'impronta digitale nell'indirizzo del server non corrisponde al certificato. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3964,7 +4137,8 @@ snd error text For chat profile %@: Per il profilo di chat %@: - servers error + servers error +servers warning For console @@ -4105,6 +4279,10 @@ Errore: %2$@ GIF e adesivi No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Ricevi una notifica quando menzionato. @@ -4347,6 +4525,7 @@ Errore: %2$@ If you joined or created channels, they will stop working permanently. + Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente. down migration warning @@ -4606,7 +4785,15 @@ Altri miglioramenti sono in arrivo! Invalid name! Nome non valido! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4719,6 +4906,10 @@ Altri miglioramenti sono in arrivo! entra come %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Entra nel gruppo @@ -4806,6 +4997,14 @@ Questo è il tuo link per il gruppo %@! Esci swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Esci dalla chat @@ -5101,6 +5300,10 @@ Questo è il tuo link per il gruppo %@! Bozza del messaggio No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Messaggio inoltrato @@ -5421,6 +5624,10 @@ Questo è il tuo link per il gruppo %@! Una nuova esperienza di chat 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nuova richiesta di contatto @@ -5496,6 +5703,14 @@ Questo è il tuo link per il gruppo %@! Nessuna password dell'app Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Nessuna chat @@ -5646,6 +5861,10 @@ Questo è il tuo link per il gruppo %@! Nessun identificatore utente. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Non compatibile! @@ -5861,6 +6080,10 @@ Richiede l'attivazione della VPN. Apri le modifiche No comment provided by engineer. + + Open channel + new chat action + Open chat Apri chat @@ -5901,6 +6124,10 @@ Richiede l'attivazione della VPN. Apri migrazione ad un altro dispositivo authentication reason + + Open new channel + new chat action + Open new chat Apri una chat nuova @@ -5993,6 +6220,14 @@ Richiede l'attivazione della VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count Conteggio PING @@ -6202,6 +6437,14 @@ Errore: %@ Conserva la bozza dell'ultimo messaggio, con gli allegati. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Indirizzo server preimpostato @@ -6287,6 +6530,10 @@ Errore: %@ Scadenza dell'instradamento privato alert title + + Proceed + alert action + Profile and server connections Profilo e connessioni al server @@ -6636,6 +6883,22 @@ swipe action Rifiutare il membro? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Il server relay viene usato solo se necessario. Un altro utente può osservare il tuo indirizzo IP. @@ -6646,6 +6909,10 @@ swipe action Il server relay protegge il tuo indirizzo IP, ma può osservare la durata della chiamata. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Rimuovi @@ -6686,6 +6953,14 @@ swipe action Rimuovere la password dal portachiavi? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Rimuove i messaggi e blocca i membri. @@ -6947,6 +7222,10 @@ chat item action Salva (e informa i membri) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Salvare le impostazioni di ammissione? @@ -6972,6 +7251,14 @@ chat item action Salva e aggiorna il profilo del gruppo No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Salva il profilo del gruppo @@ -7432,6 +7719,10 @@ chat item action Il protocollo del server è cambiato. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Il server richiede l'autorizzazione di creare code, controlla la password. @@ -7628,6 +7919,10 @@ chat item action Condividi il profilo No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Condividi questo link di invito una tantum @@ -7813,9 +8108,8 @@ chat item action Protocolli di SimpleX esaminati da Trail of Bits. No comment provided by engineer. - - SimpleX relay link - Link del relay SimpleX + + SimpleX relay address simplex link type @@ -7991,6 +8285,23 @@ report reason Iscritto No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Errori di iscrizione @@ -8091,6 +8402,10 @@ report reason Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Tocca Entra nel gruppo @@ -8139,13 +8454,18 @@ report reason Test failed at step %@. Test fallito al passo %@. - server test failure + relay test failure +server test failure Test notifications Prova le notifiche No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Prova server @@ -8198,6 +8518,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.L'app protegge la tua privacy usando diversi operatori in ogni conversazione. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion). @@ -8388,6 +8712,14 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo gruppo non esiste più. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile. @@ -8610,6 +8942,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Sbloccare il membro? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Messaggi non consegnati @@ -8862,6 +9198,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa per i messaggi No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Usa per connessioni nuove @@ -8902,6 +9242,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa l'instradamento privato con server sconosciuti. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Usa il server @@ -8942,6 +9286,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Utilizzo dei server SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Verifica il codice con il desktop @@ -9062,6 +9410,14 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Messaggio vocale… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... In attesa del desktop... @@ -9364,6 +9720,10 @@ Ripetere la richiesta di ingresso? Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini. @@ -9409,6 +9769,10 @@ Ripetere la richiesta di ingresso? Non puoi inviare messaggi! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Non è stato possibile verificarti, riprova. @@ -9521,6 +9885,10 @@ Ripetere la richiesta di connessione? Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata. @@ -9566,6 +9934,10 @@ Ripetere la richiesta di connessione? Le tue chiamate No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Il tuo database della chat @@ -9651,6 +10023,11 @@ Ripetere la richiesta di connessione? Il tuo profilo No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Verrà condiviso il tuo profilo **%@**. @@ -9676,6 +10053,14 @@ Ripetere la richiesta di connessione? Il tuo profilo casuale No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address L'indirizzo del tuo server @@ -9721,6 +10106,10 @@ Ripetere la richiesta di connessione? sopra, quindi scegli: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ %@ accettato @@ -9741,6 +10130,10 @@ Ripetere la richiesta di connessione? ti ha accettato/a rcv group event chat item + + active + No comment provided by engineer. + admin amministratore @@ -9887,6 +10280,14 @@ marked deleted chat item preview text cambio indirizzo… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored colorato @@ -10033,6 +10434,10 @@ pref value eliminato deleted chat item + + deleted channel + rcv group event chat item + deleted contact contatto eliminato @@ -10143,6 +10548,10 @@ pref value errore No comment provided by engineer. + + error: %@ + receive error chat item + expired scaduto @@ -10150,6 +10559,7 @@ pref value failed + fallito No comment provided by engineer. @@ -10272,6 +10682,10 @@ pref value è uscito/a rcv group event chat item + + link + No comment provided by engineer. + marked deleted contrassegnato eliminato @@ -10342,6 +10756,10 @@ pref value mai delete after time + + new + No comment provided by engineer. + new message messaggio nuovo @@ -10465,6 +10883,10 @@ time to disappear chiamata rifiutata call status + + relay + member role + removed rimosso @@ -10475,6 +10897,10 @@ time to disappear ha rimosso %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address indirizzo di contatto rimosso @@ -10629,6 +11055,10 @@ ultimo msg ricevuto: %2$@ non protetto No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile ha aggiornato il profilo del gruppo @@ -10649,6 +11079,10 @@ ultimo msg ricevuto: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link via link indirizzo del contatto @@ -10724,6 +11158,10 @@ ultimo msg ricevuto: %2$@ sei un osservatore No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ hai bloccato %@ 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 ddec6c47f6..a95203da15 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -185,6 +185,10 @@ %d 月 time interval + + %d relays + channel relay bar + %d sec %d 秒 @@ -200,11 +204,37 @@ %d 件のスキップされたメッセージ integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d 週 time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld No comment provided by engineer. @@ -214,6 +244,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld 件の連絡先が選択されました @@ -364,6 +398,10 @@ **QRスキャン / リンクの貼り付け**: 受け取ったリンクで接続する。 No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。 @@ -1266,6 +1304,10 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin No comment provided by engineer. @@ -1311,6 +1353,10 @@ swipe action あなたと連絡相手が音声メッセージを送信できます。 No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! ブルガリア語、フィンランド語、タイ語、ウクライナ語 - ユーザーと [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)に感謝します! @@ -1482,6 +1528,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat チャット @@ -1560,6 +1650,22 @@ set passcode view ユーザープロフィール No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme チャットテーマ @@ -1603,6 +1709,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. サーバのアドレスを確認してから再度試してください。 @@ -1735,6 +1849,10 @@ set passcode view ICEサーバを設定 No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators No comment provided by engineer. @@ -1791,7 +1909,8 @@ set passcode view Connect 接続 - server test step + relay test step +server test step Connect automatically @@ -2045,7 +2164,7 @@ This is your own one-time link! Correct name to %@? - No comment provided by engineer. + alert message Create @@ -2098,6 +2217,14 @@ This is your own one-time link! プロフィールを作成する No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue キューの作成 @@ -2128,6 +2255,10 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2287,6 +2418,10 @@ This is your own one-time link! 分散型 No comment provided by engineer. + + Decode link + relay test step + Decryption error 復号化エラー @@ -2330,6 +2465,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2491,6 +2634,10 @@ alert button 待ち行列を削除 server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2830,6 +2977,10 @@ chat item action 編集する chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile グループのプロフィールを編集 @@ -2863,6 +3014,10 @@ chat item action TCP keep-aliveを有効にする No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? 自動メッセージ削除を有効にしますか? @@ -2999,6 +3154,10 @@ chat item action パスコードを入力 No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. 正しいパスフレーズを入力してください。 @@ -3022,6 +3181,10 @@ chat item action 上にパスワードを入力すると表示されます! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually サーバを手動で入力 @@ -3073,6 +3236,10 @@ chat item action メンバー追加にエラー発生 No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3125,6 +3292,10 @@ chat item action アドレス作成にエラー発生 No comment provided by engineer. + + Error creating channel + alert title + Error creating group グループの作成エラー @@ -3250,10 +3421,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file ファイル受信にエラー発生 @@ -3293,6 +3460,10 @@ chat item action ICEサーバ保存にエラー発生 No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3426,7 +3597,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3643,7 +3815,8 @@ snd error text Fingerprint in server address does not match certificate. サーバアドレスの証明証IDが正しくないかもしれません - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3685,7 +3858,8 @@ snd error text For chat profile %@: - servers error + servers error +servers warning For console @@ -3804,6 +3978,10 @@ Error: %2$@ GIFとステッカー No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. @@ -4267,7 +4445,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4376,6 +4562,10 @@ More improvements are coming soon! %@ として参加 No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group グループに参加 @@ -4455,6 +4645,14 @@ This is your link for group %@! 脱退 swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4722,6 +4920,10 @@ This is your link for group %@! メッセージの下書き No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -5005,6 +5207,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request 新しい繋がりのリクエスト @@ -5075,6 +5281,14 @@ This is your link for group %@! アプリのパスワードはありません Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5206,6 +5420,10 @@ This is your link for group %@! 世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。 No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5406,6 +5624,10 @@ VPN を有効にする必要があります。 Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat チャットを開く @@ -5440,6 +5662,10 @@ VPN を有効にする必要があります。 Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5513,6 +5739,14 @@ VPN を有効にする必要があります。 %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING回数 @@ -5704,6 +5938,14 @@ Error: %@ 添付を含めて、下書きを保存する。 No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address プレセットサーバのアドレス @@ -5778,6 +6020,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections プロフィールとサーバ接続 @@ -6096,6 +6342,22 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. 中継サーバーは必要な場合にのみ使用されます。 別の当事者があなたの IP アドレスを監視できます。 @@ -6106,6 +6368,10 @@ swipe action リレー サーバーは IP アドレスを保護しますが、通話時間は監視されます。 No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove 削除 @@ -6142,6 +6408,14 @@ swipe action キーチェーンからパスフレーズを削除しますか? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6373,6 +6647,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6396,6 +6674,14 @@ chat item action グループプロファイルの保存と更新 No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile グループプロフィールの保存 @@ -6803,6 +7089,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. キューを作成するにはサーバーの認証が必要です。パスワードを確認してください @@ -6978,6 +7268,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -7144,8 +7438,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7304,6 +7598,23 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7392,6 +7703,10 @@ report reason Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group No comment provided by engineer. @@ -7435,12 +7750,17 @@ report reason Test failed at step %@. テストはステップ %@ で失敗しました。 - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server テストサーバ @@ -7491,6 +7811,10 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7662,6 +7986,14 @@ It can happen because of some bug or when the connection is compromised.このグループはもう存在しません。 No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7855,6 +8187,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8082,6 +8418,10 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections 新しい接続に使う @@ -8117,6 +8457,10 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server サーバを使う @@ -8151,6 +8495,10 @@ To connect, please ask your contact to create another connection link and check SimpleX チャット サーバーを使用する。 No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8260,6 +8608,14 @@ To connect, please ask your contact to create another connection link and check 音声メッセージ… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... No comment provided by engineer. @@ -8527,6 +8883,10 @@ Repeat join request? 設定からロック画面の通知プレビューを設定できます。 No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. リンク、またはQRコードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。 @@ -8569,6 +8929,10 @@ Repeat join request? メッセージを送信できませんでした! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. 確認できませんでした。 もう一度お試しください。 @@ -8673,6 +9037,10 @@ Repeat connection request? ミュートされたプロフィールがアクティブな場合でも、そのプロフィールからの通話や通知は引き続き受信します。 No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8716,6 +9084,10 @@ Repeat connection request? あなたの通話 No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database あなたのチャットデータベース @@ -8794,6 +9166,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. あなたのプロファイル **%@** が共有されます。 @@ -8818,6 +9195,14 @@ Repeat connection request? あなたのランダム・プロフィール No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address あなたのサーバアドレス @@ -8862,6 +9247,10 @@ Repeat connection request? 上で選んでください: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8879,6 +9268,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin 管理者 @@ -9013,6 +9406,14 @@ marked deleted chat item preview text アドレスを変更しています… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored 色付き @@ -9153,6 +9554,10 @@ pref value 削除完了 deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9261,6 +9666,10 @@ pref value エラー No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9384,6 +9793,10 @@ pref value 脱退 rcv group event chat item + + link + No comment provided by engineer. + marked deleted 削除済みとマーク @@ -9450,6 +9863,10 @@ pref value 一度も delete after time + + new + No comment provided by engineer. + new message 新しいメッセージ @@ -9563,6 +9980,10 @@ time to disappear 拒否した通話 call status + + relay + member role + removed 除名されました @@ -9573,6 +9994,10 @@ time to disappear %@ を除名されました rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address profile update event chat item @@ -9704,6 +10129,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile グループプロフィールを更新しました @@ -9722,6 +10151,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link 連絡先アドレスリンク経由 @@ -9793,6 +10226,10 @@ last received msg: %2$@ あなたはオブザーバーです No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item 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 e12cb0a483..60d888e309 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -185,6 +185,10 @@ %d maanden time interval + + %d relays + channel relay bar + %d sec %d sec @@ -200,11 +204,37 @@ %d overgeslagen bericht(en) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d weken time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld contact(en) geselecteerd @@ -365,6 +399,10 @@ **Link scannen/plakken**: om verbinding te maken via een link die u hebt ontvangen. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Waarschuwing**: voor directe push meldingen is een wachtwoord vereist dat is opgeslagen in de Keychain. @@ -1309,6 +1347,10 @@ swipe action Lid blokkeren? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Geblokkeerd door beheerder @@ -1357,6 +1399,10 @@ swipe action Zowel jij als je contact kunnen spraak berichten verzenden. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1542,6 +1588,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Chat @@ -1627,6 +1717,22 @@ set passcode view Gebruikers profiel No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Chat thema @@ -1676,6 +1782,14 @@ set passcode view Controleer berichten indien toegestaan. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Controleer het server adres en probeer het opnieuw. @@ -1821,6 +1935,10 @@ set passcode view ICE servers configureren No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Serveroperators configureren @@ -1884,7 +2002,8 @@ set passcode view Connect Verbind - server test step + relay test step +server test step Connect automatically @@ -2162,7 +2281,7 @@ Dit is uw eigen eenmalige link! Correct name to %@? Juiste naam voor %@? - No comment provided by engineer. + alert message Create @@ -2219,6 +2338,14 @@ Dit is uw eigen eenmalige link! Maak een profiel aan No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Maak een wachtrij @@ -2253,6 +2380,10 @@ Dit is uw eigen eenmalige link! Archief link maken No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Link maken… @@ -2416,6 +2547,10 @@ Dit is uw eigen eenmalige link! Gedecentraliseerd No comment provided by engineer. + + Decode link + relay test step + Decryption error Decodering fout @@ -2462,6 +2597,14 @@ swipe action Verwijderen en contact op de hoogte stellen No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Chat verwijderen @@ -2631,6 +2774,10 @@ alert button Wachtrij verwijderen server test step + + Delete relay + No comment provided by engineer. + Delete report Rapport verwijderen @@ -3005,6 +3152,10 @@ chat item action Bewerk chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Groep profiel bewerken @@ -3039,6 +3190,10 @@ chat item action Schakel TCP keep-alive in No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Automatisch verwijderen van berichten aanzetten? @@ -3183,6 +3338,10 @@ chat item action Voer toegangscode in No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Voer het juiste wachtwoord in. @@ -3208,6 +3367,10 @@ chat item action Voer hier boven het wachtwoord in om weer te geven! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Voer de server handmatig in @@ -3263,6 +3426,10 @@ chat item action Fout bij het toevoegen van leden No comment provided by engineer. + + Error adding relay + alert title + Error adding server Fout bij toevoegen server @@ -3320,6 +3487,10 @@ chat item action Fout bij aanmaken van adres No comment provided by engineer. + + Error creating channel + alert title + Error creating group Fout bij maken van groep @@ -3455,10 +3626,6 @@ chat item action Fout bij het openen van de chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file Fout bij ontvangen van bestand @@ -3503,6 +3670,10 @@ chat item action Fout bij opslaan van ICE servers No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Fout bij het opslaan van chatlijst @@ -3646,7 +3817,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3886,7 +4058,8 @@ snd error text Fingerprint in server address does not match certificate. Mogelijk is de certificaat vingerafdruk in het server adres onjuist - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3930,7 +4103,8 @@ snd error text For chat profile %@: Voor chatprofiel %@: - servers error + servers error +servers warning For console @@ -4071,6 +4245,10 @@ Fout: %2$@ GIF's en stickers No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Ontvang een melding als u vermeld wordt. @@ -4570,7 +4748,15 @@ Binnenkort meer verbeteringen! Invalid name! Ongeldige naam! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4682,6 +4868,10 @@ Binnenkort meer verbeteringen! deelnemen als %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Word lid van groep @@ -4768,6 +4958,14 @@ Dit is jouw link voor groep %@! Verlaten swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Chat verlaten @@ -5057,6 +5255,10 @@ Dit is jouw link voor groep %@! Concept bericht No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Bericht doorgestuurd @@ -5375,6 +5577,10 @@ Dit is jouw link voor groep %@! Nieuwe chatervaring 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nieuw contactverzoek @@ -5449,6 +5655,14 @@ Dit is jouw link voor groep %@! Geen app wachtwoord Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Geen chats @@ -5598,6 +5812,10 @@ Dit is jouw link voor groep %@! Geen gebruikers-ID's. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Niet compatibel! @@ -5811,6 +6029,10 @@ Vereist het inschakelen van VPN. Wijzigingen openen No comment provided by engineer. + + Open channel + new chat action + Open chat Chat openen @@ -5849,6 +6071,10 @@ Vereist het inschakelen van VPN. Open de migratie naar een ander apparaat authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5935,6 +6161,14 @@ Vereist het inschakelen van VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING count @@ -6144,6 +6378,14 @@ Fout: %@ Bewaar het laatste berichtconcept, met bijlagen. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Vooraf ingesteld server adres @@ -6228,6 +6470,10 @@ Fout: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections Profiel- en serververbindingen @@ -6576,6 +6822,22 @@ swipe action Lid afwijzen? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Relay server wordt alleen gebruikt als dat nodig is. Een andere partij kan uw IP-adres zien. @@ -6586,6 +6848,10 @@ swipe action Relay server beschermt uw IP-adres, maar kan de duur van het gesprek observeren. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Verwijderen @@ -6624,6 +6890,14 @@ swipe action Wachtwoord van de keychain verwijderen? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6882,6 +7156,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Toegangsinstellingen opslaan? @@ -6907,6 +7185,14 @@ chat item action Groep profiel opslaan en bijwerken No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Groep profiel opslaan @@ -7357,6 +7643,10 @@ chat item action Serverprotocol gewijzigd. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord @@ -7550,6 +7840,10 @@ chat item action Profiel delen No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Deel deze eenmalige uitnodigingslink @@ -7732,8 +8026,8 @@ chat item action SimpleX-protocollen beoordeeld door Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7909,6 +8203,23 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Subscription fouten @@ -8005,6 +8316,10 @@ report reason Tik op SimpleX-adres maken in het menu om het later te maken. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group No comment provided by engineer. @@ -8052,13 +8367,18 @@ report reason Test failed at step %@. Test mislukt bij stap %@. - server test failure + relay test failure +server test failure Test notifications Testmeldingen No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Server test @@ -8110,6 +8430,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De app beschermt uw privacy door in elk gesprek andere operatoren te gebruiken. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). De app vraagt om downloads van onbekende bestandsservers (behalve .onion) te bevestigen. @@ -8299,6 +8623,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Deze groep bestaat niet meer. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen. @@ -8516,6 +8848,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Lid deblokkeren? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Niet afgeleverde berichten @@ -8762,6 +9098,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik voor berichten No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Gebruik voor nieuwe verbindingen @@ -8801,6 +9141,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik privéroutering met onbekende servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Gebruik server @@ -8841,6 +9185,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik SimpleX Chat servers. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Code verifiëren met desktop @@ -8960,6 +9308,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Spraakbericht… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Wachten op desktop... @@ -9259,6 +9615,10 @@ Deelnameverzoek herhalen? U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. U kunt een link of een QR-code delen. Iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert. @@ -9304,6 +9664,10 @@ Deelnameverzoek herhalen? Je kunt geen berichten versturen! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. U kon niet worden geverifieerd; probeer het opnieuw. @@ -9415,6 +9779,10 @@ Verbindingsverzoek herhalen? U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard. @@ -9459,6 +9827,10 @@ Verbindingsverzoek herhalen? Uw oproepen No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Uw chat database @@ -9541,6 +9913,11 @@ Verbindingsverzoek herhalen? Jouw profiel No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Uw profiel **%@** wordt gedeeld. @@ -9566,6 +9943,14 @@ Verbindingsverzoek herhalen? Je willekeurige profiel No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Uw server adres @@ -9611,6 +9996,10 @@ Verbindingsverzoek herhalen? hier boven, kies dan: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ geaccepteerd %@ @@ -9631,6 +10020,10 @@ Verbindingsverzoek herhalen? heb je geaccepteerd rcv group event chat item + + active + No comment provided by engineer. + admin Beheerder @@ -9777,6 +10170,14 @@ marked deleted chat item preview text adres wijzigen… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored gekleurd @@ -9922,6 +10323,10 @@ pref value verwijderd deleted chat item + + deleted channel + rcv group event chat item + deleted contact verwijderd contact @@ -10032,6 +10437,10 @@ pref value fout No comment provided by engineer. + + error: %@ + receive error chat item + expired verlopen @@ -10160,6 +10569,10 @@ pref value is vertrokken rcv group event chat item + + link + No comment provided by engineer. + marked deleted gemarkeerd als verwijderd @@ -10230,6 +10643,10 @@ pref value nooit delete after time + + new + No comment provided by engineer. + new message nieuw bericht @@ -10352,6 +10769,10 @@ time to disappear geweigerde oproep call status + + relay + member role + removed verwijderd @@ -10362,6 +10783,10 @@ time to disappear verwijderd %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address contactadres verwijderd @@ -10513,6 +10938,10 @@ laatst ontvangen bericht: %2$@ onbeschermd No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile bijgewerkt groep profiel @@ -10533,6 +10962,10 @@ laatst ontvangen bericht: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link via contact adres link @@ -10608,6 +11041,10 @@ laatst ontvangen bericht: %2$@ je bent waarnemer No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ je hebt %@ geblokkeerd 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 ee17f807ba..57d22dd63b 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -185,6 +185,10 @@ %d miesięcy time interval + + %d relays + channel relay bar + %d sec %d sek @@ -200,11 +204,37 @@ %d pominięte wiadomość(i) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d tygodni time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld wybrany(e) kontakt(y) @@ -365,6 +399,10 @@ **Zeskanuj / Wklej link**: aby połączyć się za pomocą otrzymanego linku. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Uwaga**: Natychmiastowe powiadomienia push wymagają zapisania kodu dostępu w Keychain. @@ -1317,6 +1355,10 @@ swipe action Zablokować członka? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Zablokowany przez admina @@ -1367,6 +1409,10 @@ swipe action Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bułgarski, fiński, tajski i ukraiński – dzięki użytkownikom i [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1554,6 +1600,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Czat @@ -1639,6 +1729,22 @@ set passcode view Profil użytkownika No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Motyw czatu @@ -1689,6 +1795,14 @@ set passcode view Sprawdź wiadomości, gdy będzie to dopuszczone. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Sprawdź adres serwera i spróbuj ponownie. @@ -1834,6 +1948,10 @@ set passcode view Skonfiguruj serwery ICE No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Skonfiguruj operatorów serwerów @@ -1897,7 +2015,8 @@ set passcode view Connect Połącz - server test step + relay test step +server test step Connect automatically @@ -2025,6 +2144,7 @@ To jest twój jednorazowy link! Connection failed + Połączenie nie powiodło się No comment provided by engineer. @@ -2177,7 +2297,7 @@ To jest twój jednorazowy link! Correct name to %@? Poprawić imię na %@? - No comment provided by engineer. + alert message Create @@ -2234,6 +2354,14 @@ To jest twój jednorazowy link! Utwórz profil No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Utwórz kolejkę @@ -2269,6 +2397,10 @@ To jest twój jednorazowy link! Tworzenie linku archiwum No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Tworzenie linku… @@ -2432,6 +2564,10 @@ To jest twój jednorazowy link! Zdecentralizowane No comment provided by engineer. + + Decode link + relay test step + Decryption error Błąd odszyfrowania @@ -2478,6 +2614,14 @@ swipe action Usuń i powiadom kontakt No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Usuń czat @@ -2649,6 +2793,10 @@ alert button Usuń kolejkę server test step + + Delete relay + No comment provided by engineer. + Delete report Usuń raport @@ -3025,6 +3173,10 @@ chat item action Edytuj chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Edytuj profil grupy @@ -3060,6 +3212,10 @@ chat item action Włącz utrzymywanie aktywności TCP No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Czy włączyć automatyczne usuwanie wiadomości? @@ -3205,6 +3361,10 @@ chat item action Wprowadź Pin No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Wprowadź poprawne hasło. @@ -3230,6 +3390,10 @@ chat item action Wprowadź hasło powyżej, aby pokazać! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Wprowadź serwer ręcznie @@ -3285,6 +3449,10 @@ chat item action Błąd dodawania członka(ów) No comment provided by engineer. + + Error adding relay + alert title + Error adding server Błąd podczas dodawania serwera @@ -3345,6 +3513,10 @@ chat item action Błąd tworzenia adresu No comment provided by engineer. + + Error creating channel + alert title + Error creating group Błąd tworzenia grupy @@ -3480,11 +3652,6 @@ chat item action Błąd otwierania czatu No comment provided by engineer. - - Error opening group - Błąd otwierania grupy - No comment provided by engineer. - Error receiving file Błąd odbioru pliku @@ -3530,6 +3697,10 @@ chat item action Błąd zapisu serwerów ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Błąd zapisywania listy czatów @@ -3675,7 +3846,8 @@ snd error text Error: %@. Błąd: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3919,7 +4091,8 @@ snd error text Fingerprint in server address does not match certificate. Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3964,7 +4137,8 @@ snd error text For chat profile %@: Dla profilu czatu %@: - servers error + servers error +servers warning For console @@ -4105,6 +4279,10 @@ Błąd: %2$@ GIF-y i naklejki No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Otrzymuj powiadomienia, gdy ktoś wspomni o Tobie. @@ -4347,6 +4525,7 @@ Błąd: %2$@ If you joined or created channels, they will stop working permanently. + Jeśli dołączyłeś do kanałów lub je utworzyłeś, przestaną one działać na stałe. down migration warning @@ -4606,7 +4785,15 @@ Wkrótce pojawią się kolejne ulepszenia! Invalid name! Nieprawidłowa nazwa! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4719,6 +4906,10 @@ Wkrótce pojawią się kolejne ulepszenia! dołącz jako %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Dołącz do grupy @@ -4806,6 +4997,14 @@ To jest twój link do grupy %@! Opuść swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Opuść czat @@ -5101,6 +5300,10 @@ To jest twój link do grupy %@! Wersja robocza wiadomości No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Wiadomość przekazana @@ -5421,6 +5624,10 @@ To jest twój link do grupy %@! Nowe możliwości czatu 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Nowa prośba o kontakt @@ -5496,6 +5703,14 @@ To jest twój link do grupy %@! Brak hasła aplikacji Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Żadnych czatów @@ -5646,6 +5861,10 @@ To jest twój link do grupy %@! Brak identyfikatorów użytkownika. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Nie kompatybilny! @@ -5861,6 +6080,10 @@ Wymaga włączenia VPN. Otwórz zmiany No comment provided by engineer. + + Open channel + new chat action + Open chat Otwórz czat @@ -5901,6 +6124,10 @@ Wymaga włączenia VPN. Otwórz migrację na innym urządzeniu authentication reason + + Open new channel + new chat action + Open new chat Otwórz nowy czat @@ -5993,6 +6220,14 @@ Wymaga włączenia VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count Liczba PINGÓW @@ -6202,6 +6437,14 @@ Błąd: %@ Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Wstępnie ustawiony adres serwera @@ -6287,6 +6530,10 @@ Błąd: %@ Limit czasu routingu prywatnego alert title + + Proceed + alert action + Profile and server connections Profil i połączenia z serwerem @@ -6636,6 +6883,22 @@ swipe action Odrzucić członka? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Serwer przekaźnikowy jest używany tylko w razie potrzeby. Inna strona może obserwować Twój adres IP. @@ -6646,6 +6909,10 @@ swipe action Serwer przekaźnikowy chroni Twój adres IP, ale może obserwować czas trwania połączenia. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Usuń @@ -6686,6 +6953,14 @@ swipe action Usunąć hasło z pęku kluczy? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Usuwa wiadomości i blokuje członków. @@ -6947,6 +7222,10 @@ chat item action Zapisz (i powiadom członków) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Zapisać ustawienia wstępu? @@ -6972,6 +7251,14 @@ chat item action Zapisz i zaktualizuj profil grupowy No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Zapisz profil grupy @@ -7432,6 +7719,10 @@ chat item action Protokół serwera zmieniony. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło. @@ -7628,6 +7919,10 @@ chat item action Udostępnij profil No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Udostępnij ten jednorazowy link @@ -7813,9 +8108,8 @@ chat item action Protokoły SimpleX sprawdzone przez Trail of Bits. No comment provided by engineer. - - SimpleX relay link - łącze przekaźnikowe SimpleX + + SimpleX relay address simplex link type @@ -7991,6 +8285,23 @@ report reason Zasubskrybowano No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Błędy subskrypcji @@ -8091,6 +8402,10 @@ report reason Dotknij Stwórz adres SimpleX w menu aby utworzyć go później. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Dotknij Dołącz do grupy @@ -8139,13 +8454,18 @@ report reason Test failed at step %@. Test nie powiódł się na etapie %@. - server test failure + relay test failure +server test failure Test notifications Powiadomienia testowe No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Przetestuj serwer @@ -8198,6 +8518,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aplikacja chroni Twoją prywatność, korzystając z różnych operatorów w każdej rozmowie. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Aplikacja zapyta o potwierdzenie pobierania od nieznanych serwerów plików (poza .onion). @@ -8388,6 +8712,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Ta grupa już nie istnieje. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Ten link wymaga nowszej wersji aplikacji. Zaktualizuj aplikację lub poproś osobę kontaktową o przesłanie kompatybilnego łącza. @@ -8610,6 +8942,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Odblokować członka? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Niedostarczone wiadomości @@ -8862,6 +9198,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Użyj dla wiadomości No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Użyj dla nowych połączeń @@ -8902,6 +9242,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Używaj prywatnego trasowania z nieznanymi serwerami. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Użyj serwera @@ -8942,6 +9286,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Używanie serwerów SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Zweryfikuj kod z komputera @@ -9062,6 +9410,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wiadomość głosowa… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Oczekiwanie na komputer... @@ -9364,6 +9720,10 @@ Powtórzyć prośbę dołączenia? Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Możesz udostępnić link lub kod QR - każdy będzie mógł dołączyć do grupy. Nie stracisz członków grupy, jeśli później ją usuniesz. @@ -9409,6 +9769,10 @@ Powtórzyć prośbę dołączenia? Nie możesz wysyłać wiadomości! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Nie można zweryfikować użytkownika; proszę spróbować ponownie. @@ -9521,6 +9885,10 @@ Powtórzyć prośbę połączenia? Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Przestaniesz otrzymywać wiadomości z tego czatu. Historia czatu zostanie zachowana. @@ -9566,6 +9934,10 @@ Powtórzyć prośbę połączenia? Twoje połączenia No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Twoja baza danych czatu @@ -9651,6 +10023,11 @@ Powtórzyć prośbę połączenia? Twój profil No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Twój profil **%@** zostanie udostępniony. @@ -9676,6 +10053,14 @@ Powtórzyć prośbę połączenia? Twój losowy profil No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Twój adres serwera @@ -9721,6 +10106,10 @@ Powtórzyć prośbę połączenia? powyżej, a następnie wybierz: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ zaakceptowano %@ @@ -9741,6 +10130,10 @@ Powtórzyć prośbę połączenia? przyjął cię rcv group event chat item + + active + No comment provided by engineer. + admin administrator @@ -9887,6 +10280,14 @@ marked deleted chat item preview text zmiana adresu… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored kolorowy @@ -10033,6 +10434,10 @@ pref value usunięty deleted chat item + + deleted channel + rcv group event chat item + deleted contact usunięto kontakt @@ -10143,6 +10548,10 @@ pref value błąd No comment provided by engineer. + + error: %@ + receive error chat item + expired wygasły @@ -10150,6 +10559,7 @@ pref value failed + nieudane No comment provided by engineer. @@ -10272,6 +10682,10 @@ pref value opuścił rcv group event chat item + + link + No comment provided by engineer. + marked deleted zaznaczona jako usunięta @@ -10342,6 +10756,10 @@ pref value nigdy delete after time + + new + No comment provided by engineer. + new message nowa wiadomość @@ -10465,6 +10883,10 @@ time to disappear odrzucone połączenie call status + + relay + member role + removed usunięty @@ -10475,6 +10897,10 @@ time to disappear usunięto %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address usunięto adres kontaktu @@ -10629,6 +11055,10 @@ ostatnia otrzymana wiadomość: %2$@ niezabezpieczony No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile zaktualizowano profil grupy @@ -10649,6 +11079,10 @@ ostatnia otrzymana wiadomość: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link przez link adresu kontaktu @@ -10724,6 +11158,10 @@ ostatnia otrzymana wiadomość: %2$@ jesteś obserwatorem No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ zablokowałeś %@ 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 d9a5c48dda..64f2546b30 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -185,6 +185,10 @@ %d мес. time interval + + %d relays + channel relay bar + %d sec %d сек @@ -200,11 +204,37 @@ %d пропущенных сообщение(й) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d недель time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected Выбрано контактов: %lld @@ -365,6 +399,10 @@ **Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain. @@ -1315,6 +1353,10 @@ swipe action Заблокировать члена группы? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Заблокирован администратором @@ -1365,6 +1407,10 @@ swipe action Вы и Ваш контакт можете отправлять голосовые сообщения. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Болгарский, финский, тайский и украинский - благодаря пользователям и [Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1552,6 +1598,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Разговор @@ -1637,6 +1727,22 @@ set passcode view Профиль чата No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Тема чата @@ -1687,6 +1793,14 @@ set passcode view Проверять сообщения по возможности. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Проверьте адрес сервера и попробуйте снова. @@ -1832,6 +1946,10 @@ set passcode view Настройка ICE серверов No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Настроить операторов серверов @@ -1895,7 +2013,8 @@ set passcode view Connect Соединиться - server test step + relay test step +server test step Connect automatically @@ -2175,7 +2294,7 @@ This is your own one-time link! Correct name to %@? Исправить имя на %@? - No comment provided by engineer. + alert message Create @@ -2232,6 +2351,14 @@ This is your own one-time link! Создать профиль No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Создание очереди @@ -2267,6 +2394,10 @@ This is your own one-time link! Создание ссылки на архив No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Создаётся ссылка… @@ -2430,6 +2561,10 @@ This is your own one-time link! Децентрализованный No comment provided by engineer. + + Decode link + relay test step + Decryption error Ошибка расшифровки @@ -2476,6 +2611,14 @@ swipe action Удалить и уведомить контакт No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Удалить разговор @@ -2645,6 +2788,10 @@ alert button Удаление очереди server test step + + Delete relay + No comment provided by engineer. + Delete report Удалить сообщение о нарушении @@ -3021,6 +3168,10 @@ chat item action Редактировать chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Редактировать профиль группы @@ -3056,6 +3207,10 @@ chat item action Включить TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Включить автоматическое удаление сообщений? @@ -3201,6 +3356,10 @@ chat item action Введите Код No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Введите правильный пароль. @@ -3226,6 +3385,10 @@ chat item action Введите пароль выше, чтобы раскрыть! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Ввести сервер вручную @@ -3281,6 +3444,10 @@ chat item action Ошибка при добавлении членов группы No comment provided by engineer. + + Error adding relay + alert title + Error adding server Ошибка добавления сервера @@ -3341,6 +3508,10 @@ chat item action Ошибка при создании адреса No comment provided by engineer. + + Error creating channel + alert title + Error creating group Ошибка при создании группы @@ -3476,11 +3647,6 @@ chat item action Ошибка при открытии чата No comment provided by engineer. - - Error opening group - Ошибка при открытии группы - No comment provided by engineer. - Error receiving file Ошибка при получении файла @@ -3526,6 +3692,10 @@ chat item action Ошибка при сохранении ICE серверов No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Ошибка сохранения списка чатов @@ -3671,7 +3841,8 @@ snd error text Error: %@. Ошибка: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3914,7 +4085,8 @@ snd error text Fingerprint in server address does not match certificate. Возможно, хэш сертификата в адресе сервера неверный. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3959,7 +4131,8 @@ snd error text For chat profile %@: Для профиля чата %@: - servers error + servers error +servers warning For console @@ -4100,6 +4273,10 @@ Error: %2$@ ГИФ файлы и стикеры No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Уведомления, когда Вас упомянули. @@ -4599,7 +4776,15 @@ More improvements are coming soon! Invalid name! Неверное имя! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4711,6 +4896,10 @@ More improvements are coming soon! вступить как %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Вступить в группу @@ -4798,6 +4987,14 @@ This is your link for group %@! Выйти swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Покинуть разговор @@ -5091,6 +5288,10 @@ This is your link for group %@! Черновик сообщения No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Сообщение переслано @@ -5411,6 +5612,10 @@ This is your link for group %@! Новый интерфейс 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Новый запрос на соединение @@ -5486,6 +5691,14 @@ This is your link for group %@! Нет кода доступа Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Нет чатов @@ -5636,6 +5849,10 @@ This is your link for group %@! Без идентификаторов пользователей. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Несовместимая версия! @@ -5851,6 +6068,10 @@ Requires compatible VPN. Открыть изменения No comment provided by engineer. + + Open channel + new chat action + Open chat Открыть чат @@ -5891,6 +6112,10 @@ Requires compatible VPN. Открытие миграции на другое устройство authentication reason + + Open new channel + new chat action + Open new chat Открыть новый чат @@ -5983,6 +6208,14 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count Количество PING @@ -6192,6 +6425,14 @@ Error: %@ Сохранить последний черновик, вместе с вложениями. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Адрес сервера по умолчанию @@ -6277,6 +6518,10 @@ Error: %@ Таймаут конфиденциальной доставки alert title + + Proceed + alert action + Profile and server connections Профиль и соединения на сервере @@ -6626,6 +6871,22 @@ swipe action Отклонить участника? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Relay сервер используется только при необходимости. Другая сторона может видеть Ваш IP адрес. @@ -6636,6 +6897,10 @@ swipe action Relay сервер защищает Ваш IP адрес, но может отслеживать продолжительность звонка. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Удалить @@ -6675,6 +6940,14 @@ swipe action Удалить пароль из Keychain? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Может удалять сообщения и блокировать членов. @@ -6936,6 +7209,10 @@ chat item action Сохранить (и уведомить членов) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Сохранить настройки вступления? @@ -6961,6 +7238,14 @@ chat item action Сохранить сообщение и обновить группу No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Сохранить профиль группы @@ -7416,6 +7701,10 @@ chat item action Протокол сервера изменен. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Сервер требует авторизации для создания очередей, проверьте пароль. @@ -7612,6 +7901,10 @@ chat item action Поделиться профилем No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Поделиться одноразовой ссылкой-приглашением @@ -7797,9 +8090,8 @@ chat item action Аудит SimpleX протоколов от Trail of Bits. No comment provided by engineer. - - SimpleX relay link - Ссылка SimpleX relay + + SimpleX relay address simplex link type @@ -7975,6 +8267,23 @@ report reason Подписано No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Ошибки подписки @@ -8075,6 +8384,10 @@ report reason Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Нажмите Вступить в группу @@ -8123,13 +8436,18 @@ report reason Test failed at step %@. Ошибка теста на шаге %@. - server test failure + relay test failure +server test failure Test notifications Протестировать уведомления No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Тестировать сервер @@ -8182,6 +8500,10 @@ It can happen because of some bug or when the connection is compromised.Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов). @@ -8372,6 +8694,14 @@ It can happen because of some bug or when the connection is compromised.Эта группа больше не существует. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку. @@ -8594,6 +8924,10 @@ You will be prompted to complete authentication before this feature is enabled.< Разблокировать члена группы? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Недоставленные сообщения @@ -8846,6 +9180,10 @@ To connect, please ask your contact to create another connection link and check Использовать для сообщений No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Использовать для новых соединений @@ -8886,6 +9224,10 @@ To connect, please ask your contact to create another connection link and check Использовать конфиденциальную доставку с неизвестными серверами. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Использовать сервер @@ -8926,6 +9268,10 @@ To connect, please ask your contact to create another connection link and check Используются серверы, предоставленные SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Сверьте код с компьютером @@ -9045,6 +9391,14 @@ To connect, please ask your contact to create another connection link and check Голосовое сообщение… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Ожидается подключение компьютера... @@ -9347,6 +9701,10 @@ Repeat join request? Вы можете установить просмотр уведомлений на экране блокировки в настройках. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились. @@ -9392,6 +9750,10 @@ Repeat join request? Вы не можете отправлять сообщения! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Верификация не удалась; пожалуйста, попробуйте ещё раз. @@ -9504,6 +9866,10 @@ Repeat connection request? Вы все равно получите звонки и уведомления в профилях без звука, когда они активные. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Вы прекратите получать сообщения в этом разговоре. История будет сохранена. @@ -9549,6 +9915,10 @@ Repeat connection request? Ваши звонки No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database База данных @@ -9634,6 +10004,11 @@ Repeat connection request? Ваш профиль No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Будет отправлен Ваш профиль **%@**. @@ -9659,6 +10034,14 @@ Repeat connection request? Случайный профиль No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Адрес Вашего сервера @@ -9704,6 +10087,10 @@ Repeat connection request? наверху, затем выберите: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ принят %@ @@ -9724,6 +10111,10 @@ Repeat connection request? Вы приняты rcv group event chat item + + active + No comment provided by engineer. + admin админ @@ -9870,6 +10261,14 @@ marked deleted chat item preview text смена адреса… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored цвет @@ -10016,6 +10415,10 @@ pref value удалено deleted chat item + + deleted channel + rcv group event chat item + deleted contact удалил(а) контакт @@ -10126,6 +10529,10 @@ pref value ошибка No comment provided by engineer. + + error: %@ + receive error chat item + expired истекло @@ -10255,6 +10662,10 @@ pref value покинул(а) группу rcv group event chat item + + link + No comment provided by engineer. + marked deleted помечено к удалению @@ -10325,6 +10736,10 @@ pref value никогда delete after time + + new + No comment provided by engineer. + new message новое сообщение @@ -10448,6 +10863,10 @@ time to disappear отклонённый звонок call status + + relay + member role + removed удален(а) @@ -10458,6 +10877,10 @@ time to disappear удалил(а) %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address удалён адрес контакта @@ -10612,6 +11035,10 @@ last received msg: %2$@ незащищённый No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile обновил(а) профиль группы @@ -10632,6 +11059,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link через ссылку-контакт @@ -10707,6 +11138,10 @@ last received msg: %2$@ только чтение сообщений No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ Вы заблокировали %@ 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 13d3240daf..6ffea9646d 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -167,6 +167,10 @@ %d เดือน time interval + + %d relays + channel relay bar + %d sec %d วินาที @@ -181,11 +185,37 @@ %d ข้อความที่ถูกข้าม integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d สัปดาห์ time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -196,6 +226,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected % ผู้ติดต่อ LLD ที่เลือกไว้ @@ -334,6 +368,10 @@ **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain @@ -1187,6 +1225,10 @@ swipe action Block member? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin No comment provided by engineer. @@ -1232,6 +1274,10 @@ swipe action ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้ No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. @@ -1399,6 +1445,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat No comment provided by engineer. @@ -1475,6 +1565,22 @@ set passcode view โปรไฟล์ผู้ใช้ No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme No comment provided by engineer. @@ -1516,6 +1622,14 @@ set passcode view Check messages when allowed. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง @@ -1643,6 +1757,10 @@ set passcode view กำหนดค่าเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators No comment provided by engineer. @@ -1699,7 +1817,8 @@ set passcode view Connect เชื่อมต่อ - server test step + relay test step +server test step Connect automatically @@ -1941,7 +2060,7 @@ This is your own one-time link! Correct name to %@? - No comment provided by engineer. + alert message Create @@ -1992,6 +2111,14 @@ This is your own one-time link! Create profile No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue สร้างคิว @@ -2022,6 +2149,10 @@ This is your own one-time link! Creating archive link No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… No comment provided by engineer. @@ -2178,6 +2309,10 @@ This is your own one-time link! กระจายอำนาจแล้ว No comment provided by engineer. + + Decode link + relay test step + Decryption error ข้อผิดพลาดในการ decrypt @@ -2221,6 +2356,14 @@ swipe action Delete and notify contact No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat No comment provided by engineer. @@ -2382,6 +2525,10 @@ alert button ลบคิว server test step + + Delete relay + No comment provided by engineer. + Delete report No comment provided by engineer. @@ -2717,6 +2864,10 @@ chat item action แก้ไข chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile แก้ไขโปรไฟล์กลุ่ม @@ -2750,6 +2901,10 @@ chat item action เปิดใช้งาน TCP Keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? เปิดใช้งานการลบข้อความอัตโนมัติ? @@ -2884,6 +3039,10 @@ chat item action ใส่รหัสผ่าน No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. ใส่รหัสผ่านที่ถูกต้อง @@ -2907,6 +3066,10 @@ chat item action ใส่รหัสผ่านด้านบนเพื่อแสดง! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually ใส่เซิร์ฟเวอร์ด้วยตนเอง @@ -2958,6 +3121,10 @@ chat item action เกิดข้อผิดพลาดในการเพิ่มสมาชิก No comment provided by engineer. + + Error adding relay + alert title + Error adding server alert title @@ -3010,6 +3177,10 @@ chat item action เกิดข้อผิดพลาดในการสร้างที่อยู่ No comment provided by engineer. + + Error creating channel + alert title + Error creating group เกิดข้อผิดพลาดในการสร้างกลุ่ม @@ -3134,10 +3305,6 @@ chat item action Error opening chat No comment provided by engineer. - - Error opening group - No comment provided by engineer. - Error receiving file เกิดข้อผิดพลาดในการรับไฟล์ @@ -3177,6 +3344,10 @@ chat item action เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list alert title @@ -3310,7 +3481,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3527,7 +3699,8 @@ snd error text Fingerprint in server address does not match certificate. อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3569,7 +3742,8 @@ snd error text For chat profile %@: - servers error + servers error +servers warning For console @@ -3688,6 +3862,10 @@ Error: %2$@ GIFs และสติกเกอร์ No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. No comment provided by engineer. @@ -4150,7 +4328,15 @@ More improvements are coming soon! Invalid name! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4258,6 +4444,10 @@ More improvements are coming soon! เข้าร่วมเป็น %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group เข้าร่วมกลุ่ม @@ -4337,6 +4527,14 @@ This is your link for group %@! ออกจาก swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat No comment provided by engineer. @@ -4605,6 +4803,10 @@ This is your link for group %@! ร่างข้อความ No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded item status text @@ -4884,6 +5086,10 @@ This is your link for group %@! New chat experience 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request คำขอติดต่อใหม่ @@ -4953,6 +5159,14 @@ This is your link for group %@! ไม่มีรหัสผ่านสำหรับแอป Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats No comment provided by engineer. @@ -5083,6 +5297,10 @@ This is your link for group %@! แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! No comment provided by engineer. @@ -5280,6 +5498,10 @@ Requires compatible VPN. Open changes No comment provided by engineer. + + Open channel + new chat action + Open chat เปิดแชท @@ -5314,6 +5536,10 @@ Requires compatible VPN. Open migration to another device authentication reason + + Open new channel + new chat action + Open new chat new chat action @@ -5387,6 +5613,14 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count จํานวน PING @@ -5578,6 +5812,14 @@ Error: %@ เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า @@ -5651,6 +5893,10 @@ Error: %@ Private routing timeout alert title + + Proceed + alert action + Profile and server connections การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์ @@ -5968,6 +6214,22 @@ swipe action Reject member? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. ใช้เซิร์ฟเวอร์รีเลย์ในกรณีที่จำเป็นเท่านั้น บุคคลอื่นสามารถสังเกตที่อยู่ IP ของคุณได้ @@ -5978,6 +6240,10 @@ swipe action เซิร์ฟเวอร์รีเลย์ปกป้องที่อยู่ IP ของคุณ แต่สามารถสังเกตระยะเวลาของการโทรได้ No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove ลบ @@ -6014,6 +6280,14 @@ swipe action ลบรหัสผ่านออกจาก keychain หรือไม่? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. No comment provided by engineer. @@ -6245,6 +6519,10 @@ chat item action Save (and notify members) alert button + + Save (and notify subscribers) + alert button + Save admission settings? alert title @@ -6268,6 +6546,14 @@ chat item action บันทึกและอัปเดตโปรไฟล์กลุ่ม No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile บันทึกโปรไฟล์กลุ่ม @@ -6680,6 +6966,10 @@ chat item action Server protocol changed. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน @@ -6855,6 +7145,10 @@ chat item action Share profile No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -7020,8 +7314,8 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7178,6 +7472,23 @@ report reason Subscribed No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors No comment provided by engineer. @@ -7266,6 +7577,10 @@ report reason Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group No comment provided by engineer. @@ -7309,12 +7624,17 @@ report reason Test failed at step %@. การทดสอบล้มเหลวในขั้นตอน %@ - server test failure + relay test failure +server test failure Test notifications No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server เซิร์ฟเวอร์ทดสอบ @@ -7366,6 +7686,10 @@ It can happen because of some bug or when the connection is compromised.The app protects your privacy by using different operators in each conversation. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -7536,6 +7860,14 @@ It can happen because of some bug or when the connection is compromised.ไม่มีกลุ่มนี้แล้ว No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7729,6 +8061,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -7955,6 +8291,10 @@ To connect, please ask your contact to create another connection link and check Use for messages No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections ใช้สำหรับการเชื่อมต่อใหม่ @@ -7989,6 +8329,10 @@ To connect, please ask your contact to create another connection link and check Use private routing with unknown servers. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server ใช้เซิร์ฟเวอร์ @@ -8023,6 +8367,10 @@ To connect, please ask your contact to create another connection link and check กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่ No comment provided by engineer. + + Verify + relay test step + Verify code with desktop No comment provided by engineer. @@ -8132,6 +8480,14 @@ To connect, please ask your contact to create another connection link and check ข้อความเสียง… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... No comment provided by engineer. @@ -8398,6 +8754,10 @@ Repeat join request? คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง @@ -8440,6 +8800,10 @@ Repeat join request? คุณไม่สามารถส่งข้อความได้! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง. @@ -8543,6 +8907,10 @@ Repeat connection request? คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. No comment provided by engineer. @@ -8586,6 +8954,10 @@ Repeat connection request? การโทรของคุณ No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database ฐานข้อมูลการแชทของคุณ @@ -8664,6 +9036,11 @@ Repeat connection request? Your profile No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. No comment provided by engineer. @@ -8687,6 +9064,14 @@ Repeat connection request? โปรไฟล์แบบสุ่มของคุณ No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address ที่อยู่เซิร์ฟเวอร์ของคุณ @@ -8731,6 +9116,10 @@ Repeat connection request? ด้านบน จากนั้นเลือก: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -8748,6 +9137,10 @@ Repeat connection request? accepted you rcv group event chat item + + active + No comment provided by engineer. + admin ผู้ดูแลระบบ @@ -8882,6 +9275,14 @@ marked deleted chat item preview text กำลังเปลี่ยนที่อยู่… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored มีสี @@ -9022,6 +9423,10 @@ pref value ลบแล้ว deleted chat item + + deleted channel + rcv group event chat item + deleted contact rcv direct event chat item @@ -9129,6 +9534,10 @@ pref value ผิดพลาด No comment provided by engineer. + + error: %@ + receive error chat item + expired No comment provided by engineer. @@ -9252,6 +9661,10 @@ pref value ออกแล้ว rcv group event chat item + + link + No comment provided by engineer. + marked deleted ทำเครื่องหมายว่าลบแล้ว @@ -9318,6 +9731,10 @@ pref value ไม่เคย delete after time + + new + No comment provided by engineer. + new message ข้อความใหม่ @@ -9431,6 +9848,10 @@ time to disappear สายถูกปฏิเสธ call status + + relay + member role + removed ถูกลบแล้ว @@ -9441,6 +9862,10 @@ time to disappear ถูกลบแล้ว %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address profile update event chat item @@ -9572,6 +9997,10 @@ last received msg: %2$@ unprotected No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile อัปเดตโปรไฟล์กลุ่มแล้ว @@ -9590,6 +10019,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link ผ่านลิงค์ที่อยู่ติดต่อ @@ -9661,6 +10094,10 @@ last received msg: %2$@ คุณเป็นผู้สังเกตการณ์ No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ snd group event chat item 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 c97da9e0b5..f8890ca077 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -185,6 +185,10 @@ %d ay time interval + + %d relays + channel relay bar + %d sec %d saniye @@ -200,11 +204,37 @@ %d okunmamış mesaj(lar) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d hafta time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld kişi seçildi @@ -365,6 +399,10 @@ edindiğiniz bağlantı aracılığıyla bağlanmak için **Linki tarayın/yapıştırın**. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Dikkat**: Anında iletilen bildirimlere Anahtar Zinciri'nde kaydedilmiş parola gereklidir. @@ -1315,6 +1353,10 @@ swipe action Üyeyi engelle? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Yönetici tarafından engellendi @@ -1365,6 +1407,10 @@ swipe action Sen ve konuştuğun kişi sesli mesaj gönderebilir. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Bulgarca, Fince, Tayca ve Ukraynaca - kullanıcılara ve [Weblate] e teşekkürler! (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1552,6 +1598,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Sohbet @@ -1637,6 +1727,22 @@ set passcode view Kullanıcı profili No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Sohbet teması @@ -1687,6 +1793,14 @@ set passcode view İzin verildiğinde mesajları kontrol et. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Sunucu adresini kontrol edip tekrar deneyin. @@ -1832,6 +1946,10 @@ set passcode view ICE sunucularını ayarla No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Sunucu operatörlerini yapılandır @@ -1895,7 +2013,8 @@ set passcode view Connect Bağlan - server test step + relay test step +server test step Connect automatically @@ -2175,7 +2294,7 @@ Bu senin kendi tek kullanımlık bağlantın! Correct name to %@? İsim %@ olarak düzeltilsin mi? - No comment provided by engineer. + alert message Create @@ -2232,6 +2351,14 @@ Bu senin kendi tek kullanımlık bağlantın! Profil oluştur No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Sıra oluştur @@ -2267,6 +2394,10 @@ Bu senin kendi tek kullanımlık bağlantın! Arşiv bağlantısı oluşturuluyor No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Link oluşturuluyor… @@ -2430,6 +2561,10 @@ Bu senin kendi tek kullanımlık bağlantın! Merkezi Olmayan No comment provided by engineer. + + Decode link + relay test step + Decryption error Şifre çözme hatası @@ -2476,6 +2611,14 @@ swipe action Sil ve kişiye bildir No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Sohbeti sil @@ -2645,6 +2788,10 @@ alert button Sırayı sil server test step + + Delete relay + No comment provided by engineer. + Delete report Raporu sil @@ -3021,6 +3168,10 @@ chat item action Düzenle chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Grup profilini düzenle @@ -3056,6 +3207,10 @@ chat item action TCP canlı tutmayı etkinleştir No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Otomatik mesaj silme etkinleştirilsin mi? @@ -3201,6 +3356,10 @@ chat item action Şifre gir No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Doğru şifreyi gir. @@ -3226,6 +3385,10 @@ chat item action Göstermek için yukarıdaki şifreyi gir! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Sunucuya manuel olarak gir @@ -3281,6 +3444,10 @@ chat item action Üye(ler) eklenirken hata oluştu No comment provided by engineer. + + Error adding relay + alert title + Error adding server Sunucu eklenirken hata oluştu @@ -3340,6 +3507,10 @@ chat item action Adres oluşturulurken hata oluştu No comment provided by engineer. + + Error creating channel + alert title + Error creating group Grup oluşturulurken hata oluştu @@ -3475,11 +3646,6 @@ chat item action Kişiyi hazırlama hatası No comment provided by engineer. - - Error opening group - Grubu hazırlama hatası - No comment provided by engineer. - Error receiving file Dosya alınırken sorun oluştu @@ -3525,6 +3691,10 @@ chat item action ICE sunucularını kaydedirken sorun oluştu No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Sohbet listesini kaydetme hatası @@ -3669,7 +3839,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3910,7 +4081,8 @@ snd error text Fingerprint in server address does not match certificate. Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3954,7 +4126,8 @@ snd error text For chat profile %@: Sohbet profili için %@: - servers error + servers error +servers warning For console @@ -4095,6 +4268,10 @@ Hata: %2$@ GİFler ve çıkartmalar No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Bahsedildiğinde bildirim alın. @@ -4595,7 +4772,15 @@ Daha fazla iyileştirme yakında geliyor! Invalid name! Geçersiz isim! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4707,6 +4892,10 @@ Daha fazla iyileştirme yakında geliyor! %@ olarak katıl No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Gruba katıl @@ -4794,6 +4983,14 @@ Bu senin grup için bağlantın %@! Ayrıl swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Sohbetten ayrıl @@ -5087,6 +5284,10 @@ Bu senin grup için bağlantın %@! Mesaj taslağı No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Mesaj iletildi @@ -5407,6 +5608,10 @@ Bu senin grup için bağlantın %@! Yeni bir sohbet deneyimi 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Yeni bağlantı isteği @@ -5482,6 +5687,14 @@ Bu senin grup için bağlantın %@! Uygulama şifresi yok Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Hiç sohbet yok @@ -5632,6 +5845,10 @@ Bu senin grup için bağlantın %@! Herhangi bir kullanıcı tanımlayıcısı yok. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Uyumlu değil! @@ -5847,6 +6064,10 @@ VPN'nin etkinleştirilmesi gerekir. Açık değişiklikler No comment provided by engineer. + + Open channel + new chat action + Open chat Sohbeti aç @@ -5887,6 +6108,10 @@ VPN'nin etkinleştirilmesi gerekir. Başka bir cihaza açık geçiş authentication reason + + Open new channel + new chat action + Open new chat Yeni sohbet aç @@ -5979,6 +6204,14 @@ VPN'nin etkinleştirilmesi gerekir. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING sayısı @@ -6188,6 +6421,14 @@ Hata: %@ Son mesaj taslağını ekleriyle birlikte koru. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Ön ayarlı sunucu adresi @@ -6273,6 +6514,10 @@ Hata: %@ Özel yönlendirme zaman aşımı alert title + + Proceed + alert action + Profile and server connections Profil ve sunucu bağlantıları @@ -6622,6 +6867,22 @@ swipe action Üyeyi reddet? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Yönlendirici sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir. @@ -6632,6 +6893,10 @@ swipe action Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Sil @@ -6671,6 +6936,14 @@ swipe action Anahtar Zinciri'ndeki parola silinsin mi? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Mesajları kaldırır ve üyeleri engeller. @@ -6932,6 +7205,10 @@ chat item action Kaydet (ve üyelere bildir) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Kabul ayarlarını kaydet? @@ -6957,6 +7234,14 @@ chat item action Kaydet ve grup profilini güncelle No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Grup profilini kaydet @@ -7412,6 +7697,10 @@ chat item action Sunucu protokolü değişti. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin @@ -7608,6 +7897,10 @@ chat item action Profil paylaş No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Bu tek kullanımlık bağlantı davetini paylaş @@ -7793,9 +8086,8 @@ chat item action SimpleX protokolleri Trail of Bits tarafından incelenmiştir. No comment provided by engineer. - - SimpleX relay link - SimpleX aktarıcı bağlantısı + + SimpleX relay address simplex link type @@ -7971,6 +8263,23 @@ report reason Abone olundu No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Abone olurken hata @@ -8071,6 +8380,10 @@ report reason Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Gruba katıl'a dokunun @@ -8119,13 +8432,18 @@ report reason Test failed at step %@. Test %@ adımında başarısız oldu. - server test failure + relay test failure +server test failure Test notifications Bildirimleri test et No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Sunucuyu test et @@ -8178,6 +8496,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Uygulama, her sohbette farklı operatörler kullanarak gizliliğinizi korur. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Uygulama bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion hariç). @@ -8368,6 +8690,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu grup artık mevcut değildir. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Bu bağlantı daha yeni bir uygulama sürümü gerektiriyor. Lütfen uygulamayı güncelleyin veya kişinizden uyumlu bir bağlantı göndermesini isteyin. @@ -8589,6 +8919,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Üyenin engeli kaldırılsın mı? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Teslim edilmemiş mesajlar @@ -8841,6 +9175,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Mesajlar için kullan No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Yeni bağlantılar için kullan @@ -8881,6 +9219,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Bilinmeyen sunucularla gizli yönlendirme kullan. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Sunucu kullan @@ -8921,6 +9263,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste SimpleX Chat sunucuları kullanılıyor. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Bilgisayarla kodu doğrula @@ -9040,6 +9386,14 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Sesli mesaj… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Bilgisayar için bekleniyor... @@ -9340,6 +9694,10 @@ Katılma isteği tekrarlansın mı? Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Bir bağlantı veya QR kodu paylaşabilirsiniz - bu durumda herkes gruba katılabilir. Daha sonra silseniz bile grubun üyelerini kaybetmezsiniz. @@ -9385,6 +9743,10 @@ Katılma isteği tekrarlansın mı? Mesajlar gönderemezsiniz! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Doğrulanamadınız; lütfen tekrar deneyin. @@ -9497,6 +9859,10 @@ Bağlantı isteği tekrarlansın mı? Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Bu sohbetten mesaj almaya son vereceksiniz. Sohbet geçmişi korunacaktır. @@ -9542,6 +9908,10 @@ Bağlantı isteği tekrarlansın mı? Aramaların No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Sohbet veritabanınız @@ -9627,6 +9997,11 @@ Bağlantı isteği tekrarlansın mı? Profiliniz No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Profiliniz **%@** paylaşılacaktır. @@ -9652,6 +10027,14 @@ Bağlantı isteği tekrarlansın mı? Rasgele profiliniz No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Sunucu adresiniz @@ -9697,6 +10080,10 @@ Bağlantı isteği tekrarlansın mı? yukarı çıkın, ardından seçin: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ kabul edildi %@ @@ -9717,6 +10104,10 @@ Bağlantı isteği tekrarlansın mı? seni kabul etti rcv group event chat item + + active + No comment provided by engineer. + admin yönetici @@ -9863,6 +10254,14 @@ marked deleted chat item preview text adres değiştiriliyor… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored renklendirilmiş @@ -10009,6 +10408,10 @@ pref value silindi deleted chat item + + deleted channel + rcv group event chat item + deleted contact silinmiş kişi @@ -10119,6 +10522,10 @@ pref value hata No comment provided by engineer. + + error: %@ + receive error chat item + expired süresi dolmuş @@ -10248,6 +10655,10 @@ pref value ayrıldı rcv group event chat item + + link + No comment provided by engineer. + marked deleted silinmiş olarak işaretlenmiş @@ -10318,6 +10729,10 @@ pref value asla delete after time + + new + No comment provided by engineer. + new message yeni mesaj @@ -10440,6 +10855,10 @@ time to disappear geri çevrilmiş çağrı call status + + relay + member role + removed kaldırıldı @@ -10450,6 +10869,10 @@ time to disappear %@ kaldırıldı rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address kişi adresi silindi @@ -10604,6 +11027,10 @@ son alınan msj: %2$@ korumasız No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile grup profili güncellendi @@ -10624,6 +11051,10 @@ son alınan msj: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link bağlantı adres uzantısı ile @@ -10699,6 +11130,10 @@ son alınan msj: %2$@ gözlemcisiniz No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ engelledin %@ 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 9cc95a6085..839b4e2652 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -185,6 +185,10 @@ %d місяців time interval + + %d relays + channel relay bar + %d sec %d сек @@ -200,11 +204,37 @@ %d пропущено повідомлення(ь) integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d тижнів time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld контакт(и) вибрані @@ -365,6 +399,10 @@ **Відсканувати / Вставити посилання**: підключитися за отриманим посиланням. No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку. @@ -1313,6 +1351,10 @@ swipe action Заблокувати користувача? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin Заблокований адміністратором @@ -1361,6 +1403,10 @@ swipe action Надсилати голосові повідомлення можете як ви, так і ваш контакт. No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! Болгарською, фінською, тайською та українською мовами - завдяки користувачам та [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1548,6 +1594,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat Чат @@ -1633,6 +1723,22 @@ set passcode view Профіль користувача No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme Тема чату @@ -1683,6 +1789,14 @@ set passcode view Перевірте повідомлення, коли це дозволено. No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. Перевірте адресу сервера та спробуйте ще раз. @@ -1828,6 +1942,10 @@ set passcode view Налаштування серверів ICE No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators Налаштувати операторів сервера @@ -1891,7 +2009,8 @@ set passcode view Connect Підключіться - server test step + relay test step +server test step Connect automatically @@ -2170,7 +2289,7 @@ This is your own one-time link! Correct name to %@? Виправити ім'я на %@? - No comment provided by engineer. + alert message Create @@ -2227,6 +2346,14 @@ This is your own one-time link! Створити профіль No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue Створити чергу @@ -2262,6 +2389,10 @@ This is your own one-time link! Створення архівного посилання No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… Створення посилання… @@ -2425,6 +2556,10 @@ This is your own one-time link! Децентралізований No comment provided by engineer. + + Decode link + relay test step + Decryption error Помилка розшифровки @@ -2471,6 +2606,14 @@ swipe action Видалити та повідомити контакт No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat Видалити чат @@ -2640,6 +2783,10 @@ alert button Видалити чергу server test step + + Delete relay + No comment provided by engineer. + Delete report Видалити скаргу @@ -3015,6 +3162,10 @@ chat item action Редагувати chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile Редагування профілю групи @@ -3050,6 +3201,10 @@ chat item action Увімкнути TCP keep-alive No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? Увімкнути автоматичне видалення повідомлень? @@ -3195,6 +3350,10 @@ chat item action Введіть пароль No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. Введіть правильну парольну фразу. @@ -3220,6 +3379,10 @@ chat item action Введіть пароль вище, щоб показати! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually Увійдіть на сервер вручну @@ -3275,6 +3438,10 @@ chat item action Помилка додавання користувача(ів) No comment provided by engineer. + + Error adding relay + alert title + Error adding server Помилка додавання сервера @@ -3334,6 +3501,10 @@ chat item action Помилка створення адреси No comment provided by engineer. + + Error creating channel + alert title + Error creating group Помилка створення групи @@ -3469,11 +3640,6 @@ chat item action Помилка відкриття чату No comment provided by engineer. - - Error opening group - Помилка відкриття групи - No comment provided by engineer. - Error receiving file Помилка отримання файлу @@ -3519,6 +3685,10 @@ chat item action Помилка збереження серверів ICE No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list Помилка під час збереження списку чатів @@ -3662,7 +3832,8 @@ snd error text Error: %@. - server test error + relay test error +server test error Error: URL is invalid @@ -3902,7 +4073,8 @@ snd error text Fingerprint in server address does not match certificate. Відбиток в адресі сервера не співпадає з сертифікатом. - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3946,7 +4118,8 @@ snd error text For chat profile %@: Для профілю чату %@: - servers error + servers error +servers warning For console @@ -4087,6 +4260,10 @@ Error: %2$@ GIF-файли та наклейки No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. Отримуйте сповіщення, коли вас згадують. @@ -4587,7 +4764,15 @@ More improvements are coming soon! Invalid name! Неправильне ім'я! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4699,6 +4884,10 @@ More improvements are coming soon! приєднатися як %@ No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group Приєднуйтесь до групи @@ -4786,6 +4975,14 @@ This is your link for group %@! Залишити swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat Вийти з чату @@ -5077,6 +5274,10 @@ This is your link for group %@! Чернетка повідомлення No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded Повідомлення переслано @@ -5397,6 +5598,10 @@ This is your link for group %@! Новий досвід спілкування в чаті 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request Новий запит на контакт @@ -5472,6 +5677,14 @@ This is your link for group %@! Немає пароля програми Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats Без чатів @@ -5622,6 +5835,10 @@ This is your link for group %@! Ніяких ідентифікаторів користувачів. No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! Не сумісні! @@ -5835,6 +6052,10 @@ Requires compatible VPN. Відкриті зміни No comment provided by engineer. + + Open channel + new chat action + Open chat Відкритий чат @@ -5873,6 +6094,10 @@ Requires compatible VPN. Відкрита міграція на інший пристрій authentication reason + + Open new channel + new chat action + Open new chat Відкрити новий чат @@ -5964,6 +6189,14 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count Кількість PING @@ -6173,6 +6406,14 @@ Error: %@ Зберегти чернетку останнього повідомлення з вкладеннями. No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address Попередньо встановлена адреса сервера @@ -6258,6 +6499,10 @@ Error: %@ Тайм-аут приватної маршрутизації alert title + + Proceed + alert action + Profile and server connections З'єднання профілю та сервера @@ -6607,6 +6852,22 @@ swipe action Відхилити учасника? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу. @@ -6617,6 +6878,10 @@ swipe action Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка. No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove Видалити @@ -6655,6 +6920,14 @@ swipe action Видалити парольну фразу з брелока? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. Видаляє повідомлення та блокує користувачів. @@ -6916,6 +7189,10 @@ chat item action Зберегти (і повідомити учасникам) alert button + + Save (and notify subscribers) + alert button + Save admission settings? Зберегти налаштування входу? @@ -6941,6 +7218,14 @@ chat item action Збереження та оновлення профілю групи No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile Зберегти профіль групи @@ -7396,6 +7681,10 @@ chat item action Протокол сервера змінено. alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. Сервер вимагає авторизації для створення черг, перевірте пароль. @@ -7592,6 +7881,10 @@ chat item action Поділіться профілем No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link Поділіться цим одноразовим посиланням-запрошенням @@ -7777,8 +8070,8 @@ chat item action Протоколи SimpleX, розглянуті Trail of Bits. No comment provided by engineer. - - SimpleX relay link + + SimpleX relay address simplex link type @@ -7954,6 +8247,23 @@ report reason Підписано No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors Помилки підписки @@ -8053,6 +8363,10 @@ report reason Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше. No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group Натисніть Приєднатися до групи @@ -8101,13 +8415,18 @@ report reason Test failed at step %@. Тест завершився невдало на кроці %@. - server test failure + relay test failure +server test failure Test notifications Тестові сповіщення No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server Тестовий сервер @@ -8160,6 +8479,10 @@ It can happen because of some bug or when the connection is compromised.Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові. No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion). @@ -8350,6 +8673,14 @@ It can happen because of some bug or when the connection is compromised.Цієї групи більше не існує. No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання. @@ -8569,6 +8900,10 @@ You will be prompted to complete authentication before this feature is enabled.< Розблокувати учасника? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages Недоставлені повідомлення @@ -8821,6 +9156,10 @@ To connect, please ask your contact to create another connection link and check Використовуйте для повідомлень No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections Використовуйте для нових з'єднань @@ -8861,6 +9200,10 @@ To connect, please ask your contact to create another connection link and check Використовуйте приватну маршрутизацію з невідомими серверами. No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server Використовувати сервер @@ -8901,6 +9244,10 @@ To connect, please ask your contact to create another connection link and check Використання серверів SimpleX Chat. No comment provided by engineer. + + Verify + relay test step + Verify code with desktop Перевірте код на робочому столі @@ -9020,6 +9367,14 @@ To connect, please ask your contact to create another connection link and check Голосове повідомлення… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... Чекаємо на десктопну версію... @@ -9320,6 +9675,10 @@ Repeat join request? Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань. No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо згодом видалите її. @@ -9365,6 +9724,10 @@ Repeat join request? Ви не можете надсилати повідомлення! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. Вас не вдалося верифікувати, спробуйте ще раз. @@ -9477,6 +9840,10 @@ Repeat connection request? Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні. No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. Ви більше не будете отримувати повідомлення з цього чату. Історія чату буде збережена. @@ -9522,6 +9889,10 @@ Repeat connection request? Твої дзвінки No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database Ваша база даних чату @@ -9607,6 +9978,11 @@ Repeat connection request? Ваш профіль No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. Ваш профіль **%@** буде опублікований. @@ -9632,6 +10008,14 @@ Repeat connection request? Ваш випадковий профіль No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address Адреса вашого сервера @@ -9677,6 +10061,10 @@ Repeat connection request? вище, а потім обирайте: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ прийнято %@ @@ -9697,6 +10085,10 @@ Repeat connection request? прийняв(ла) вас rcv group event chat item + + active + No comment provided by engineer. + admin адмін @@ -9843,6 +10235,14 @@ marked deleted chat item preview text змінює адресу… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored кольоровий @@ -9989,6 +10389,10 @@ pref value видалено deleted chat item + + deleted channel + rcv group event chat item + deleted contact видалений контакт @@ -10099,6 +10503,10 @@ pref value помилка No comment provided by engineer. + + error: %@ + receive error chat item + expired закінчився @@ -10228,6 +10636,10 @@ pref value ліворуч rcv group event chat item + + link + No comment provided by engineer. + marked deleted з позначкою видалено @@ -10298,6 +10710,10 @@ pref value ніколи delete after time + + new + No comment provided by engineer. + new message нове повідомлення @@ -10420,6 +10836,10 @@ time to disappear відхилений виклик call status + + relay + member role + removed видалено @@ -10430,6 +10850,10 @@ time to disappear видалено %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address видалено контактну адресу @@ -10582,6 +11006,10 @@ last received msg: %2$@ незахищені No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile оновлений профіль групи @@ -10602,6 +11030,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link за посиланням на контактну адресу @@ -10677,6 +11109,10 @@ last received msg: %2$@ ви спостерігач No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ ви заблокували %@ 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 fbb118774a..d981147cfe 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 @@ -185,6 +185,10 @@ %d 月 time interval + + %d relays + channel relay bar + %d sec %d 秒 @@ -200,11 +204,37 @@ 跳过的 %d 条消息 integrity error chat item + + %d subscriber + channel subscriber count + + + %d subscribers + channel subscriber count + %d weeks %d 星期 time interval + + %1$d/%2$d relays active + channel creation progress +channel relay bar progress + + + %1$d/%2$d relays active, %3$d failed + channel creation progress with errors +channel relay bar progress with errors + + + %1$d/%2$d relays connected + channel subscriber relay bar progress + + + %1$d/%2$d relays connected, %3$d errors + channel subscriber relay bar progress with errors + %lld %lld @@ -215,6 +245,10 @@ %lld %@ No comment provided by engineer. + + %lld channel events + No comment provided by engineer. + %lld contact(s) selected %lld 联系人已选择 @@ -365,6 +399,10 @@ **扫描/粘贴链接**:用您收到的链接连接。 No comment provided by engineer. + + **Test relay** to retrieve its name. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**:及时推送通知需要保存在钥匙串的密码。 @@ -1317,6 +1355,10 @@ swipe action 封禁成员吗? No comment provided by engineer. + + Block subscriber for all? + No comment provided by engineer. + Blocked by admin 由管理员封禁 @@ -1367,6 +1409,10 @@ swipe action 您和您的联系人都可以发送语音消息。 No comment provided by engineer. + + Broadcast + compose placeholder for channel owner + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! 保加利亚语、芬兰语、泰语和乌克兰语——感谢用户和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! @@ -1554,6 +1600,50 @@ new chat action authentication reason set passcode view + + Channel + No comment provided by engineer. + + + Channel display name + No comment provided by engineer. + + + Channel full name (optional) + No comment provided by engineer. + + + Channel image + No comment provided by engineer. + + + Channel link + No comment provided by engineer. + + + Channel profile + No comment provided by engineer. + + + Channel profile is stored on subscribers' devices and on the chat relays. + No comment provided by engineer. + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + alert message + + + Channel will be deleted for all subscribers - this cannot be undone! + No comment provided by engineer. + + + Channel will be deleted for you - this cannot be undone! + No comment provided by engineer. + + + Channel will start working with %1$d of %2$d relays. Proceed? + alert message + Chat 聊天 @@ -1639,6 +1729,22 @@ set passcode view 用户资料 No comment provided by engineer. + + Chat relay + No comment provided by engineer. + + + Chat relays + No comment provided by engineer. + + + Chat relays forward messages in channels you create. + No comment provided by engineer. + + + Chat relays forward messages to channel subscribers. + No comment provided by engineer. + Chat theme 聊天主题 @@ -1689,6 +1795,14 @@ set passcode view 在被允许时检查消息。 No comment provided by engineer. + + Check relay address and try again. + alert message + + + Check relay name and try again. + alert message + Check server address and try again. 检查服务器地址并再试一次。 @@ -1834,6 +1948,10 @@ set passcode view 配置 ICE 服务器 No comment provided by engineer. + + Configure relays + No comment provided by engineer. + Configure server operators 配置服务器运营方 @@ -1897,7 +2015,8 @@ set passcode view Connect 连接 - server test step + relay test step +server test step Connect automatically @@ -2176,7 +2295,7 @@ This is your own one-time link! Correct name to %@? 将名称更正为 %@? - No comment provided by engineer. + alert message Create @@ -2233,6 +2352,14 @@ This is your own one-time link! 创建个人资料 No comment provided by engineer. + + Create public channel + No comment provided by engineer. + + + Create public channel (BETA) + No comment provided by engineer. + Create queue 创建队列 @@ -2268,6 +2395,10 @@ This is your own one-time link! 正在创建存档链接 No comment provided by engineer. + + Creating channel + No comment provided by engineer. + Creating link… 创建链接中… @@ -2431,6 +2562,10 @@ This is your own one-time link! 分散式 No comment provided by engineer. + + Decode link + relay test step + Decryption error 解密错误 @@ -2477,6 +2612,14 @@ swipe action 删除并通知联系人 No comment provided by engineer. + + Delete channel + No comment provided by engineer. + + + Delete channel? + No comment provided by engineer. + Delete chat 删除聊天 @@ -2647,6 +2790,10 @@ alert button 删除队列 server test step + + Delete relay + No comment provided by engineer. + Delete report 删除举报 @@ -3023,6 +3170,10 @@ chat item action 编辑 chat item action + + Edit channel profile + No comment provided by engineer. + Edit group profile 编辑群组资料 @@ -3058,6 +3209,10 @@ chat item action 启用 TCP 保持活跃状态 No comment provided by engineer. + + Enable at least one chat relay in Network & Servers. + channel creation warning + Enable automatic message deletion? 启用自动删除消息? @@ -3203,6 +3358,10 @@ chat item action 输入密码 No comment provided by engineer. + + Enter channel name… + No comment provided by engineer. + Enter correct passphrase. 输入正确密码。 @@ -3228,6 +3387,10 @@ chat item action 在上面输入密码以显示! No comment provided by engineer. + + Enter relay name… + No comment provided by engineer. + Enter server manually 手动输入服务器 @@ -3283,6 +3446,10 @@ chat item action 添加成员错误 No comment provided by engineer. + + Error adding relay + alert title + Error adding server 添加服务器出错 @@ -3342,6 +3509,10 @@ chat item action 创建地址错误 No comment provided by engineer. + + Error creating channel + alert title + Error creating group 创建群组错误 @@ -3477,11 +3648,6 @@ chat item action 打开聊天时出错 No comment provided by engineer. - - Error opening group - 打开群时出错 - No comment provided by engineer. - Error receiving file 接收文件错误 @@ -3527,6 +3693,10 @@ chat item action 保存 ICE 服务器错误 No comment provided by engineer. + + Error saving channel profile + No comment provided by engineer. + Error saving chat list 保存聊天列表出错 @@ -3672,7 +3842,8 @@ snd error text Error: %@. 错误:%@。 - server test error + relay test error +server test error Error: URL is invalid @@ -3916,7 +4087,8 @@ snd error text Fingerprint in server address does not match certificate. 服务器地址中的证书指纹可能不正确 - server test error + relay test error +server test error Fingerprint in server address does not match certificate: %@. @@ -3961,7 +4133,8 @@ snd error text For chat profile %@: 为聊天资料 %@: - servers error + servers error +servers warning For console @@ -4102,6 +4275,10 @@ Error: %2$@ GIF 和贴纸 No comment provided by engineer. + + Get link + relay test step + Get notified when mentioned. 被提及时收到通知。 @@ -4603,7 +4780,15 @@ More improvements are coming soon! Invalid name! 无效名称! - No comment provided by engineer. + alert title + + + Invalid relay address! + alert title + + + Invalid relay name! + alert title Invalid response @@ -4716,6 +4901,10 @@ More improvements are coming soon! 以 %@ 身份加入 No comment provided by engineer. + + Join channel + No comment provided by engineer. + Join group 加入群组 @@ -4803,6 +4992,14 @@ This is your link for group %@! 离开 swipe action + + Leave channel + No comment provided by engineer. + + + Leave channel? + No comment provided by engineer. + Leave chat 离开聊天 @@ -5097,6 +5294,10 @@ This is your link for group %@! 消息草稿 No comment provided by engineer. + + Message error + No comment provided by engineer. + Message forwarded 消息已转发 @@ -5417,6 +5618,10 @@ This is your link for group %@! 新的聊天体验 🎉 No comment provided by engineer. + + New chat relay + No comment provided by engineer. + New contact request 新联系人请求 @@ -5492,6 +5697,14 @@ This is your link for group %@! 没有应用程序密码 Authentication unavailable + + No chat relays + No comment provided by engineer. + + + No chat relays enabled. + servers warning + No chats 无聊天 @@ -5642,6 +5855,10 @@ This is your link for group %@! 没有用户标识符。 No comment provided by engineer. + + Not all relays connected + alert title + Not compatible! 不兼容! @@ -5857,6 +6074,10 @@ Requires compatible VPN. 打开更改 No comment provided by engineer. + + Open channel + new chat action + Open chat 打开聊天 @@ -5897,6 +6118,10 @@ Requires compatible VPN. 打开迁移到另一台设备 authentication reason + + Open new channel + new chat action + Open new chat 打开新聊天 @@ -5989,6 +6214,14 @@ Requires compatible VPN. %@ alert message + + Owner + No comment provided by engineer. + + + Owners + No comment provided by engineer. + PING count PING 次数 @@ -6198,6 +6431,14 @@ Error: %@ 保留最后的消息草稿及其附件。 No comment provided by engineer. + + Preset relay address + No comment provided by engineer. + + + Preset relay name + No comment provided by engineer. + Preset server address 预设服务器地址 @@ -6283,6 +6524,10 @@ Error: %@ 私密路由超时 alert title + + Proceed + alert action + Profile and server connections 资料和服务器连接 @@ -6631,6 +6876,22 @@ swipe action 拒绝成员? alert title + + Relay + No comment provided by engineer. + + + Relay address + alert title + + + Relay connection failed + alert title + + + Relay link + No comment provided by engineer. + Relay server is only used if necessary. Another party can observe your IP address. 中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。 @@ -6641,6 +6902,10 @@ swipe action 中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。 No comment provided by engineer. + + Relay test failed! + No comment provided by engineer. + Remove 移除 @@ -6681,6 +6946,14 @@ swipe action 从钥匙串中删除密码? No comment provided by engineer. + + Remove subscriber + No comment provided by engineer. + + + Remove subscriber? + alert title + Removes messages and blocks members. 删除消息并封禁成员。 @@ -6941,6 +7214,10 @@ chat item action 保存(并通知成员) alert button + + Save (and notify subscribers) + alert button + Save admission settings? 保存入群设置? @@ -6966,6 +7243,14 @@ chat item action 保存和更新组配置文件 No comment provided by engineer. + + Save channel profile + No comment provided by engineer. + + + Save channel profile? + alert title + Save group profile 保存群组资料 @@ -7426,6 +7711,10 @@ chat item action 服务器协议已更改。 alert title + + Server requires authorization to connect to relay, check password. + relay test error + Server requires authorization to create queues, check password. 服务器需要授权才能创建队列,检查密码 @@ -7622,6 +7911,10 @@ chat item action 分享资料 No comment provided by engineer. + + Share relay address + No comment provided by engineer. + Share this 1-time invite link 分享此一次性邀请链接 @@ -7807,9 +8100,8 @@ chat item action SimpleX 协议由 Trail of Bits 审阅。 No comment provided by engineer. - - SimpleX relay link - SimpleX 中继链接 + + SimpleX relay address simplex link type @@ -7984,6 +8276,23 @@ report reason 已订阅 No comment provided by engineer. + + Subscriber + No comment provided by engineer. + + + Subscriber will be removed from channel - this cannot be undone! + alert message + + + Subscribers + No comment provided by engineer. + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + No comment provided by engineer. + Subscription errors 订阅错误 @@ -8084,6 +8393,10 @@ report reason 要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址” No comment provided by engineer. + + Tap Join channel + No comment provided by engineer. + Tap Join group 轻按加入群 @@ -8132,13 +8445,18 @@ report reason Test failed at step %@. 在步骤 %@ 上测试失败。 - server test failure + relay test failure +server test failure Test notifications 测试通知 No comment provided by engineer. + + Test relay + No comment provided by engineer. + Test server 测试服务器 @@ -8191,6 +8509,10 @@ It can happen because of some bug or when the connection is compromised.应用通过在每个对话中使用不同运营方保护你的隐私。 No comment provided by engineer. + + The app removed this message after %lld attempts to receive it. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). 该应用程序将要求确认从未知文件服务器(.onion 除外)下载。 @@ -8379,6 +8701,14 @@ It can happen because of some bug or when the connection is compromised.该群组已不存在。 No comment provided by engineer. + + This is a chat relay address, it cannot be used to connect. + alert message + + + This is your link for channel %@! + new chat action + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. 此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。 @@ -8600,6 +8930,10 @@ You will be prompted to complete authentication before this feature is enabled.< 解封成员吗? No comment provided by engineer. + + Unblock subscriber for all? + No comment provided by engineer. + Undelivered messages 未送达的消息 @@ -8852,6 +9186,10 @@ To connect, please ask your contact to create another connection link and check 用于消息 No comment provided by engineer. + + Use for new channels + No comment provided by engineer. + Use for new connections 用于新连接 @@ -8892,6 +9230,10 @@ To connect, please ask your contact to create another connection link and check 对未知服务器使用私有路由。 No comment provided by engineer. + + Use relay + No comment provided by engineer. + Use server 使用服务器 @@ -8932,6 +9274,10 @@ To connect, please ask your contact to create another connection link and check 使用 SimpleX Chat 服务器。 No comment provided by engineer. + + Verify + relay test step + Verify code with desktop 用桌面端验证代码 @@ -9052,6 +9398,14 @@ To connect, please ask your contact to create another connection link and check 语音消息…… No comment provided by engineer. + + Wait + alert action + + + Wait response + relay test step + Waiting for desktop... 正在等待桌面... @@ -9354,6 +9708,10 @@ Repeat join request? 您可以通过设置来设置锁屏通知预览。 No comment provided by engineer. + + You can share a link or a QR code - anybody will be able to join the channel. + No comment provided by engineer. + You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. 您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。 @@ -9399,6 +9757,10 @@ Repeat join request? 您无法发送消息! alert title + + You connected to the channel via this relay link. + No comment provided by engineer. + You could not be verified; please try again. 您的身份无法验证,请再试一次。 @@ -9510,6 +9872,10 @@ Repeat connection request? 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 No comment provided by engineer. + + You will stop receiving messages from this channel. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this chat. Chat history will be preserved. 你将停止从这个聊天收到消息。聊天历史将被保留。 @@ -9555,6 +9921,10 @@ Repeat connection request? 您的通话 No comment provided by engineer. + + Your channel + No comment provided by engineer. + Your chat database 您的聊天数据库 @@ -9638,6 +10008,11 @@ Repeat connection request? 您的个人资料 No comment provided by engineer. + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + No comment provided by engineer. + Your profile **%@** will be shared. 您的个人资料 **%@** 将被共享。 @@ -9663,6 +10038,14 @@ Repeat connection request? 您的随机资料 No comment provided by engineer. + + Your relay address + No comment provided by engineer. + + + Your relay name + No comment provided by engineer. + Your server address 您的服务器地址 @@ -9708,6 +10091,10 @@ Repeat connection request? 上面,然后选择: No comment provided by engineer. + + accepted + No comment provided by engineer. + accepted %@ rcv group event chat item @@ -9727,6 +10114,10 @@ Repeat connection request? 接受了你 rcv group event chat item + + active + No comment provided by engineer. + admin 管理员 @@ -9873,6 +10264,14 @@ marked deleted chat item preview text 更改地址… chat item text + + channel + shown as sender role for channel messages + + + channel profile updated + snd group event chat item + colored 彩色 @@ -10019,6 +10418,10 @@ pref value 已删除 deleted chat item + + deleted channel + rcv group event chat item + deleted contact 已删除联系人 @@ -10129,6 +10532,10 @@ pref value 错误 No comment provided by engineer. + + error: %@ + receive error chat item + expired 过期 @@ -10258,6 +10665,10 @@ pref value 已离开 rcv group event chat item + + link + No comment provided by engineer. + marked deleted 标记为已删除 @@ -10328,6 +10739,10 @@ pref value 从不 delete after time + + new + No comment provided by engineer. + new message 新消息 @@ -10450,6 +10865,10 @@ time to disappear 拒接来电 call status + + relay + member role + removed 已删除 @@ -10460,6 +10879,10 @@ time to disappear 已删除 %@ rcv group event chat item + + removed (%d attempts) + receive error chat item + removed contact address 删除了联系地址 @@ -10614,6 +11037,10 @@ last received msg: %2$@ 未受保护 No comment provided by engineer. + + updated channel profile + rcv group event chat item + updated group profile 已更新的群组资料 @@ -10634,6 +11061,10 @@ last received msg: %2$@ v%@ (%@) No comment provided by engineer. + + via %@ + relay hostname + via contact address link 通过联系地址链接 @@ -10709,6 +11140,10 @@ last received msg: %2$@ 您是观察者 No comment provided by engineer. + + you are subscriber + No comment provided by engineer. + you blocked %@ 你阻止了%@ diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index fd5c4c990f..18f3e2c344 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -465,8 +465,7 @@ fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { switch self { case let .direct(contact): if contact.sendMsgToConnect { return nil } @@ -1578,6 +1577,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { if groupInfo.membership.memberActive { switch(groupChatScope) { case .none: + if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) } if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") } if groupInfo.membership.memberRole == .observer { return groupInfo.useRelays ? ("you are subscriber", nil) : ("you are observer", "Please contact group admin.") @@ -1613,10 +1613,9 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .invalidJSON: return ("can't send messages", nil) } - } } - public var sendMsgEnabled: Bool { userCantSendReason == nil } + public var sendMsgEnabled: Bool { userCantSendReason() == nil } public var incognito: Bool { get { @@ -2372,6 +2371,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var ready: Bool { get { true } } public var nextConnectPrepared: Bool { if let preparedGroup { !preparedGroup.connLinkStartedConnection } else { false } } public var profileChangeProhibited: Bool { preparedGroup?.connLinkPreparedConnection ?? false } + public var isChannel: Bool { groupProfile.isChannel } public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var shortDescr: String? { groupProfile.shortDescr } @@ -2500,6 +2500,8 @@ public struct GroupProfile: Codable, NamedChat, Hashable { set { memberAdmission = newValue } } + public var isChannel: Bool { publicGroup?.groupType == .channel } + public static let sampleData = GroupProfile( displayName: "team", fullName: "My Team" @@ -2564,6 +2566,7 @@ public enum RelayStatus: String, Decodable, Equatable, Hashable { case rsInvited = "invited" case rsAccepted = "accepted" case rsActive = "active" + case rsInactive = "inactive" } public struct RelayProfile: Codable, Equatable, Hashable { @@ -2636,6 +2639,7 @@ extension RelayStatus { case .rsInvited: "invited" case .rsAccepted: "accepted" case .rsActive: "active" + case .rsInactive: "inactive" } } } @@ -4589,10 +4593,14 @@ public enum MsgContent: Equatable, Hashable { case voice(text: String, duration: Int) case file(String) case report(text: String, reason: ReportReason) - case chat(text: String, chatLink: MsgChatLink) + case chat(text: String, chatLink: MsgChatLink, ownerSig: LinkOwnerSig?) // TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift case unknown(type: String, text: String) + public var chatLinkStr: String? { + if case let .chat(_, chatLink, _) = self { chatLink.connLinkStr } else { nil } + } + public var text: String { switch self { case let .text(text): return text @@ -4602,7 +4610,7 @@ public enum MsgContent: Equatable, Hashable { case let .voice(text, _): return text case let .file(text): return text case let .report(text, _): return text - case let .chat(text, _): return text + case let .chat(text, _, _): return text case let .unknown(_, text): return text } } @@ -4665,6 +4673,7 @@ public enum MsgContent: Equatable, Hashable { case duration case reason case chatLink + case ownerSig } public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool { @@ -4676,7 +4685,7 @@ public enum MsgContent: Equatable, Hashable { case let (.voice(lt, ld), .voice(rt, rd)): return lt == rt && ld == rd case let (.file(lf), .file(rf)): return lf == rf case let (.report(lt, lr), .report(rt, rr)): return lt == rt && lr == rr - case let (.chat(lt, ll), .chat(rt, rl)): return lt == rt && ll == rl + case let (.chat(lt, ll, ls), .chat(rt, rl, rs)): return lt == rt && ll == rl && ls == rs case let (.unknown(lType, lt), .unknown(rType, rt)): return lType == rType && lt == rt default: return false } @@ -4719,7 +4728,8 @@ extension MsgContent: Decodable { case "chat": let text = try container.decode(String.self, forKey: CodingKeys.text) let chatLink = try container.decode(MsgChatLink.self, forKey: CodingKeys.chatLink) - self = .chat(text: text, chatLink: chatLink) + let ownerSig = try container.decodeIfPresent(LinkOwnerSig.self, forKey: CodingKeys.ownerSig) + self = .chat(text: text, chatLink: chatLink, ownerSig: ownerSig) default: let text = try? container.decode(String.self, forKey: CodingKeys.text) self = .unknown(type: type, text: text ?? "unknown message format") @@ -4761,10 +4771,11 @@ extension MsgContent: Encodable { try container.encode("report", forKey: .type) try container.encode(text, forKey: .text) try container.encode(reason, forKey: .reason) - case let .chat(text, chatLink): + case let .chat(text, chatLink, ownerSig): try container.encode("chat", forKey: .type) try container.encode(text, forKey: .text) try container.encode(chatLink, forKey: .chatLink) + try container.encodeIfPresent(ownerSig, forKey: .ownerSig) // TODO use original JSON and type case let .unknown(_, text): try container.encode("text", forKey: .type) @@ -4773,7 +4784,7 @@ extension MsgContent: Encodable { } } -public enum MsgContentTag: String, Decodable { +public enum MsgContentTag: Codable, Hashable { case text case link case image @@ -4781,12 +4792,195 @@ public enum MsgContentTag: String, Decodable { case voice case file case report + case chat + case unknown(type: String) + + public var rawValue: String { + switch self { + case .text: return "text" + case .link: return "link" + case .image: return "image" + case .video: return "video" + case .voice: return "voice" + case .file: return "file" + case .report: return "report" + case .chat: return "chat" + case let .unknown(type): return type + } + } + + public init(from decoder: Decoder) throws { + let s = try decoder.singleValueContainer().decode(String.self) + switch s { + case "text": self = .text + case "link": self = .link + case "image": self = .image + case "video": self = .video + case "voice": self = .voice + case "file": self = .file + case "report": self = .report + case "chat": self = .chat + case "liveText": self = .text + default: self = .unknown(type: s) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } } -public enum MsgChatLink: Codable, Equatable, Hashable { +public enum MsgChatLink: Equatable, Hashable { case contact(connLink: String, profile: Profile, business: Bool) case invitation(invLink: String, profile: Profile) case group(connLink: String, groupProfile: GroupProfile) + + public var isPublicGroup: Bool { + if case let .group(_, gp) = self { gp.publicGroup != nil } else { false } + } + + public var connLinkStr: String { + switch self { + case let .group(connLink, _): connLink + case let .contact(connLink, _, _): connLink + case let .invitation(invLink, _): invLink + } + } + + public var image: String? { + switch self { + case let .group(_, groupProfile): groupProfile.image + case let .contact(_, profile, _): profile.image + case let .invitation(_, profile): profile.image + } + } + + public var displayName: String { + switch self { + case let .group(_, groupProfile): groupProfile.displayName + case let .contact(_, profile, _): profile.displayName + case let .invitation(_, profile): profile.displayName + } + } + + public var iconName: String { + switch self { + case let .group(_, groupProfile): + groupProfile.isChannel ? "antenna.radiowaves.left.and.right.circle.fill" : "person.2.circle.fill" + case let .contact(_, _, business): + business ? "briefcase.circle.fill" : "person.crop.circle.fill" + case .invitation: + "person.crop.circle.fill" + } + } + + public var smallIconName: String { + switch self { + case let .group(_, groupProfile): + groupProfile.isChannel ? "antenna.radiowaves.left.and.right" : "person.2" + case let .contact(_, _, business): + business ? "briefcase" : "person" + case .invitation: + "person" + } + } + + public var fullName: String { + switch self { + case let .group(_, groupProfile): groupProfile.fullName + case let .contact(_, profile, _): profile.fullName + case let .invitation(_, profile): profile.fullName + } + } + + public var shortDescription: String? { + let s: String? = switch self { + case let .group(_, groupProfile): groupProfile.shortDescr + case let .contact(_, profile, _): profile.shortDescr + case let .invitation(_, profile): profile.shortDescr + } + if let d = s?.trimmingCharacters(in: .whitespacesAndNewlines), !d.isEmpty { return d } + return nil + } + + public func infoLine(signed: Bool) -> String { + var s: String = switch self { + case let .group(_, groupProfile): + groupProfile.isChannel + ? NSLocalizedString("Channel link", comment: "chat link info line") + : NSLocalizedString("Group link", comment: "chat link info line") + case let .contact(_, _, business): + business + ? NSLocalizedString("Business address", comment: "chat link info line") + : NSLocalizedString("Contact address", comment: "chat link info line") + case .invitation: + NSLocalizedString("One-time link", comment: "chat link info line") + } + if signed { + s += " " + ( + self.isPublicGroup + ? NSLocalizedString("(from owner)", comment: "chat link info line") + : NSLocalizedString("(signed)", comment: "chat link info line") + ) + } + return s + } +} + +extension MsgChatLink: Decodable { + private enum CodingKeys: String, CodingKey { + case type, connLink, invLink, profile, business, groupProfile + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "contact": + let connLink = try container.decode(String.self, forKey: .connLink) + let profile = try container.decode(Profile.self, forKey: .profile) + let business = try container.decode(Bool.self, forKey: .business) + self = .contact(connLink: connLink, profile: profile, business: business) + case "invitation": + let invLink = try container.decode(String.self, forKey: .invLink) + let profile = try container.decode(Profile.self, forKey: .profile) + self = .invitation(invLink: invLink, profile: profile) + case "group": + let connLink = try container.decode(String.self, forKey: .connLink) + let groupProfile = try container.decode(GroupProfile.self, forKey: .groupProfile) + self = .group(connLink: connLink, groupProfile: groupProfile) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown MsgChatLink type: \(type)") + } + } +} + +extension MsgChatLink: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .contact(connLink, profile, business): + try container.encode("contact", forKey: .type) + try container.encode(connLink, forKey: .connLink) + try container.encode(profile, forKey: .profile) + try container.encode(business, forKey: .business) + case let .invitation(invLink, profile): + try container.encode("invitation", forKey: .type) + try container.encode(invLink, forKey: .invLink) + try container.encode(profile, forKey: .profile) + case let .group(connLink, groupProfile): + try container.encode("group", forKey: .type) + try container.encode(connLink, forKey: .connLink) + try container.encode(groupProfile, forKey: .groupProfile) + } + } +} + +public struct LinkOwnerSig: Codable, Equatable, Hashable { + public var ownerId: String? + public var chatBinding: String + public var ownerSig: String } public struct FormattedText: Decodable, Hashable { diff --git a/apps/ios/SimpleXChat/exported_symbols.txt b/apps/ios/SimpleXChat/exported_symbols.txt new file mode 100644 index 0000000000..52c3bf83e9 --- /dev/null +++ b/apps/ios/SimpleXChat/exported_symbols.txt @@ -0,0 +1,11 @@ +# Swift mangled symbols (Swift 5+ ABI stable prefix) +_$s* + +# ObjC class/metaclass symbols (for NSObject subclasses) +_OBJC_CLASS_$_* +_OBJC_METACLASS_$_* + +# C API (SimpleX.h bridging header) +_chat_* +_haskell_init* +_hs_init* diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index fb8529fb88..323c5c6985 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1218,7 +1218,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Потвърдено"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Свързване"; /* No comment provided by engineer. */ @@ -1389,7 +1390,7 @@ set passcode view */ /* No comment provided by engineer. */ "Core version: v%@" = "Версия на ядрото: v%@"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Поправи име на %@?"; /* No comment provided by engineer. */ @@ -2211,7 +2212,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Намирайте чатове по-бързо"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; /* No comment provided by engineer. */ @@ -2532,7 +2534,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Невалидно потвърждение за мигриране"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Невалидно име!"; /* No comment provided by engineer. */ @@ -3975,7 +3977,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Тестът е неуспешен на стъпка %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 9e1fe7139c..0031c2421b 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -196,6 +196,9 @@ /* No comment provided by engineer. */ "%lld file(s) with total size of %@" = "%lld soubor(y) s celkovou velikostí %@"; +/* No comment provided by engineer. */ +"%lld group events" = "%lld událostí skupiny"; + /* No comment provided by engineer. */ "%lld members" = "%lld členové"; @@ -305,7 +308,7 @@ time interval */ "A new random profile will be shared." = "Nový náhodný profil bude sdílen."; /* No comment provided by engineer. */ -"A separate TCP connection will be used **for each chat profile you have in the app**." = "Samostatné připojení TCP bude použito **pro každý chat profil, který máte v aplikaci**."; +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Samostatné připojení TCP bude použito **pro každý profil chatu, který máte v aplikaci**."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**pro každý kontakt a člena skupiny** bude použito samostatné připojení TCP.\n**Upozornění**: Pokud máte mnoho připojení, spotřeba baterie a provozu může být podstatně vyšší a některá připojení mohou selhat."; @@ -453,7 +456,7 @@ swipe action */ "All chats and messages will be deleted - this cannot be undone!" = "Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět!"; /* alert message */ -"All chats will be removed from the list %@, and the list deleted." = "Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn."; +"All chats will be removed from the list %@, and the list deleted." = "Všechny chaty budou odstraněny ze seznamu %@ a seznam bude smazán."; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Všechna data se při zadání vymažou."; @@ -570,7 +573,7 @@ swipe action */ "Always use relay" = "Spojení přes relé"; /* No comment provided by engineer. */ -"An empty chat profile with the provided name is created, and the app opens as usual." = "Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle."; +"An empty chat profile with the provided name is created, and the app opens as usual." = "Vytvořit prázdný profil chatu se zadaným názvem a otevřít aplikaci jako obvykle."; /* report reason */ "Another reason" = "Jiný důvod"; @@ -747,7 +750,7 @@ swipe action */ "Business address" = "Obchodní adresa"; /* No comment provided by engineer. */ -"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Podle profilu chatu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; /* No comment provided by engineer. */ "Call already ended!" = "Hovor již skončil!"; @@ -821,7 +824,7 @@ new chat action */ "Change automatic message deletion?" = "Změnit automatické mazání zpráv?"; /* authentication reason */ -"Change chat profiles" = "Změnit chat profily"; +"Change chat profiles" = "Změnit profily chatu"; /* No comment provided by engineer. */ "Change database passphrase?" = "Změnit přístupovou frázi databáze?"; @@ -959,7 +962,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm upload" = "Potvrdit nahrání"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Připojit"; /* No comment provided by engineer. */ @@ -1208,10 +1212,10 @@ swipe action */ "Delete all files" = "Odstranit všechny soubory"; /* No comment provided by engineer. */ -"Delete chat profile" = "Smazat chat profil"; +"Delete chat profile" = "Smazat profil chatu"; /* No comment provided by engineer. */ -"Delete chat profile?" = "Smazat chat profil?"; +"Delete chat profile?" = "Smazat profil chatu?"; /* No comment provided by engineer. */ "Delete connection" = "Smazat připojení"; @@ -1753,7 +1757,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Najděte chaty rychleji"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Otisk certifikátu v adrese serveru neodpovídá."; /* No comment provided by engineer. */ @@ -1859,7 +1864,7 @@ snd error text */ "Hidden" = "Skryté"; /* No comment provided by engineer. */ -"Hidden chat profiles" = "Skryté chat profily"; +"Hidden chat profiles" = "Skryté profily chatu"; /* No comment provided by engineer. */ "Hidden profile password" = "Hesla skrytých profilů"; @@ -2291,7 +2296,7 @@ snd error text */ "Most likely this connection is deleted." = "Pravděpodobně je toto spojení smazáno."; /* No comment provided by engineer. */ -"Multiple chat profiles" = "Více chatovacích profilů"; +"Multiple chat profiles" = "Více profilů chatu"; /* notification label action */ "Mute" = "Ztlumit"; @@ -2632,7 +2637,7 @@ new chat action */ "Protect app screen" = "Ochrana obrazovky aplikace"; /* No comment provided by engineer. */ -"Protect your chat profiles with a password!" = "Chraňte své chat profily heslem!"; +"Protect your chat profiles with a password!" = "Chraňte své profily chatu pomocí hesla!"; /* No comment provided by engineer. */ "Protocol timeout" = "Časový limit protokolu"; @@ -2655,6 +2660,9 @@ new chat action */ /* No comment provided by engineer. */ "Read more" = "Přečíst více"; +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Více informací v [průvodci uživatele](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/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; @@ -2775,7 +2783,7 @@ swipe action */ "Reset to defaults" = "Obnovení výchozího nastavení"; /* No comment provided by engineer. */ -"Restart the app to create a new chat profile" = "Restartujte aplikaci pro vytvoření nového chat profilu"; +"Restart the app to create a new chat profile" = "Restartujte aplikaci pro vytvoření nového profilu chatu"; /* No comment provided by engineer. */ "Restart the app to use imported chat database" = "Restartujte aplikaci pro použití importované databáze chatu"; @@ -2944,7 +2952,7 @@ chat item action */ "Sender may have deleted the connection request." = "Odesílatel možná smazal požadavek připojení."; /* No comment provided by engineer. */ -"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných chat profilech."; +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu."; /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts." = "Odesílání potvrzení o doručení bude povoleno pro všechny kontakty."; @@ -3184,7 +3192,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test selhal v kroku %@."; /* No comment provided by engineer. */ @@ -3248,7 +3257,7 @@ chat item action */ "The sender will NOT be notified" = "Odesílatel NEBUDE informován"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "Servery pro nová připojení vašeho aktuálního chat profilu **%@**."; +"The servers for new connections of your current chat profile **%@**." = "Servery pro nová připojení vašeho aktuálního profilu chatu **%@**."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Toto nastavení je pro váš aktuální profil **%@**."; @@ -3275,7 +3284,7 @@ chat item action */ "This group no longer exists." = "Tato skupina již neexistuje."; /* No comment provided by engineer. */ -"This setting applies to messages in your current chat profile **%@**." = "Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**."; +"This setting applies to messages in your current chat profile **%@**." = "Toto nastavení platí pro zprávy ve vašem aktuálním profilu chatu **%@**."; /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Chcete-li položit jakékoli dotazy a dostávat aktuality:"; @@ -3299,7 +3308,7 @@ chat item action */ "To record voice message please grant permission to use Microphone." = "Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Chat profily**."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Profily chatu**."; /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze."; @@ -3332,7 +3341,7 @@ chat item action */ "Unhide" = "Odkrýt"; /* No comment provided by engineer. */ -"Unhide chat profile" = "Odkrýt chat profil"; +"Unhide chat profile" = "Odkrýt profil chatu"; /* No comment provided by engineer. */ "Unhide profile" = "Odkrýt profil"; @@ -3680,7 +3689,7 @@ chat item action */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování."; /* No comment provided by engineer. */ -"Your chat profiles" = "Vaše chat profily"; +"Your chat profiles" = "Vaše profily chatu"; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@)."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index e3979abc37..7137ccac8b 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1248,7 +1248,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Bestätigt"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Verbinden"; /* No comment provided by engineer. */ @@ -1353,6 +1354,9 @@ set passcode view */ /* chat list item title (it should not be shown */ "connection established" = "Verbindung hergestellt"; +/* No comment provided by engineer. */ +"Connection failed" = "Verbindung fehlgeschlagen"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Die Verbindung wurde vom Serverbetreiber blockiert:\n%@"; @@ -1464,7 +1468,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Abrundung Ecken"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Richtiger Name für %@?"; /* No comment provided by engineer. */ @@ -2338,9 +2342,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Fehler beim Öffnen des Chat"; -/* No comment provided by engineer. */ -"Error opening group" = "Fehler beim Vorbereiten der Gruppe"; - /* alert title */ "Error receiving file" = "Fehler beim Herunterladen der Datei"; @@ -2454,7 +2455,8 @@ file error text snd error text */ "Error: %@" = "Fehler: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Fehler: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2504,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Exportieren des Datenbank-Archivs…"; +/* No comment provided by engineer. */ +"failed" = "Fehlgeschlagen"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Das Entfernen des Passworts ist fehlgeschlagen"; @@ -2604,7 +2609,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Fingerabdruck in der Serveradresse stimmt nicht mit dem Zertifikat überein."; /* No comment provided by engineer. */ @@ -2628,7 +2634,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Für alle Moderatoren"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Für das Chat-Profil %@:"; /* No comment provided by engineer. */ @@ -2871,6 +2878,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Wenn Sie Ihren Selbstzerstörungs-Zugangscode während des Öffnens der App eingeben:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Kanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert."; + /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Tippen Sie unten auf **Später wiederholen**, wenn Sie den Chat jetzt benötigen (es wird Ihnen angeboten, die Datenbank bei einem Neustart der App zu migrieren)."; @@ -3042,7 +3052,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Migrations-Bestätigung ungültig"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Ungültiger Name!"; /* No comment provided by engineer. */ @@ -5172,9 +5182,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Die SimpleX-Protokolle wurden von Trail of Bits überprüft."; -/* simplex link type */ -"SimpleX relay link" = "SimpleX Relais-Link"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereinfachter Inkognito-Modus"; @@ -5380,7 +5387,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Temporärer Datei-Fehler"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Der Test ist beim Schritt %@ fehlgeschlagen."; /* No comment provided by engineer. */ diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index a05bc9f4b6..42ed52c412 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1248,7 +1248,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Confirmado"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Conectar"; /* No comment provided by engineer. */ @@ -1464,7 +1465,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Esquina"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "¿Corregir el nombre a %@?"; /* No comment provided by engineer. */ @@ -2338,9 +2339,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Error al abrir chat"; -/* No comment provided by engineer. */ -"Error opening group" = "Error al abrir el grupo"; - /* alert title */ "Error receiving file" = "Error al recibir archivo"; @@ -2454,7 +2452,8 @@ file error text snd error text */ "Error: %@" = "Error: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Error: %@."; /* No comment provided by engineer. */ @@ -2604,7 +2603,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "La huella en la dirección del servidor no coincide con el certificado: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "La huella en la dirección del servidor no coincide con el certificado."; /* No comment provided by engineer. */ @@ -2628,7 +2628,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Para todos los moderadores"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Para el perfil de chat %@:"; /* No comment provided by engineer. */ @@ -3042,7 +3043,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Confirmación de migración no válida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "¡Nombre no válido!"; /* No comment provided by engineer. */ @@ -5172,9 +5173,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolos de SimpleX auditados por Trail of Bits."; -/* simplex link type */ -"SimpleX relay link" = "Enlace de servidor SimpleX"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Modo incógnito simplificado"; @@ -5380,7 +5378,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Error en archivo temporal"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Prueba no superada en el paso %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index ea3f9c4386..5f81f94fb8 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -650,7 +650,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm password" = "Vahvista salasana"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Yhdistä"; /* No comment provided by engineer. */ @@ -1429,7 +1430,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Löydä keskustelut nopeammin"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; /* No comment provided by engineer. */ @@ -2848,7 +2850,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Testi epäonnistui vaiheessa %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 2b2a1e98e5..30f28c6b8c 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1194,7 +1194,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Confirmé"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Se connecter"; /* No comment provided by engineer. */ @@ -1392,7 +1393,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Coin"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Corriger le nom pour %@ ?"; /* No comment provided by engineer. */ @@ -2466,7 +2467,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Recherche de message plus rapide"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; /* No comment provided by engineer. */ @@ -2487,7 +2489,8 @@ snd error text */ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correction non prise en charge par un membre du groupe"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Pour le profil de discussion %@ :"; /* No comment provided by engineer. */ @@ -2853,7 +2856,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Confirmation de migration invalide"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Nom invalide !"; /* No comment provided by engineer. */ @@ -4803,7 +4806,8 @@ chat item action */ /* file error alert title */ "Temporary file error" = "Erreur de fichier temporaire"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Échec du test à l'étape %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 56277f4fd3..93eb633dd7 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -543,7 +543,7 @@ swipe action */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára."; /* No comment provided by engineer. */ -"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra."; +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-átjátszókra."; /* No comment provided by engineer. */ "Allow" = "Engedélyezés"; @@ -636,7 +636,7 @@ swipe action */ "Always use private routing." = "Mindig legyen használva privát útválasztás."; /* No comment provided by engineer. */ -"Always use relay" = "Mindig legyen használva továbbítókiszolgáló"; +"Always use relay" = "Mindig legyen használva átjátszó"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Egy üres csevegési profil lesz létrehozva a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; @@ -1248,7 +1248,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Megerősítve"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Kapcsolódás"; /* No comment provided by engineer. */ @@ -1353,6 +1354,9 @@ set passcode view */ /* chat list item title (it should not be shown */ "connection established" = "kapcsolat létrehozva"; +/* No comment provided by engineer. */ +"Connection failed" = "Nem sikerült létrehozni a kapcsolatot"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "A kiszolgáló üzemeltetője letiltotta a kapcsolatot:\n%@"; @@ -1464,7 +1468,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Sarok"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Helyesbíti a nevet a következőre: %@?"; /* No comment provided by engineer. */ @@ -1834,13 +1838,13 @@ alert button */ "Desktop devices" = "Számítógépek"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbítókiszolgáló beállításaival."; +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival."; /* snd error text */ "Destination server error: %@" = "Célkiszolgáló-hiba: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbítókiszolgálóval."; +"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval."; /* No comment provided by engineer. */ "Detailed statistics" = "Részletes statisztikák"; @@ -2249,7 +2253,7 @@ chat item action */ "Error checking token status" = "Hiba történt a token állapotának ellenőrzésekor"; /* alert message */ -"Error connecting to forwarding server %@. Please try later." = "Hiba történt a(z) %@ továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; +"Error connecting to forwarding server %@. Please try later." = "Hiba történt a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; /* subscription status explanation */ "Error connecting to the server used to receive messages from this connection: %@" = "Hiba történt a kapcsolódáskor ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál: %@"; @@ -2338,9 +2342,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Hiba történt a csevegés megnyitásakor"; -/* No comment provided by engineer. */ -"Error opening group" = "Hiba történt a csoport megnyitásakor"; - /* alert title */ "Error receiving file" = "Hiba történt a fájl fogadásakor"; @@ -2454,7 +2455,8 @@ file error text snd error text */ "Error: %@" = "Hiba: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Hiba: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2504,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Adatbázis-archívum exportálása…"; +/* No comment provided by engineer. */ +"failed" = "sikertelen"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Nem sikerült eltávolítani a jelmondatot"; @@ -2599,12 +2604,13 @@ snd error text */ "Fingerprint in destination server address does not match certificate: %@." = "A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; /* No comment provided by engineer. */ -"Fingerprint in forwarding server address does not match certificate: %@." = "A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; +"Fingerprint in forwarding server address does not match certificate: %@." = "A továbbító kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal."; /* No comment provided by engineer. */ @@ -2628,7 +2634,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Az összes moderátor számára"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "A(z) %@ nevű csevegési profilhoz:"; /* No comment provided by engineer. */ @@ -2677,19 +2684,19 @@ snd error text */ "Forwarding %lld messages" = "%lld üzenet továbbítása"; /* alert message */ -"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %1$@ továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később."; +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %1$@ továbbító kiszolgáló nem tudott kapcsolódni a(z) %2$@ célkiszolgálóhoz. Próbálja meg később."; /* No comment provided by engineer. */ -"Forwarding server address is incompatible with network settings: %@." = "A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; +"Forwarding server address is incompatible with network settings: %@." = "A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; /* No comment provided by engineer. */ -"Forwarding server version is incompatible with network settings: %@." = "A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; +"Forwarding server version is incompatible with network settings: %@." = "A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; /* snd error text */ -"Forwarding server: %@\nDestination server error: %@" = "Továbbítókiszolgáló: %1$@\nCélkiszolgáló-hiba: %2$@"; +"Forwarding server: %@\nDestination server error: %@" = "Továbbító kiszolgáló: %1$@\nCélkiszolgáló-hiba: %2$@"; /* snd error text */ -"Forwarding server: %@\nError: %@" = "Továbbítókiszolgáló: %1$@\nHiba: %2$@"; +"Forwarding server: %@\nError: %@" = "Továbbító kiszolgáló: %1$@\nHiba: %2$@"; /* No comment provided by engineer. */ "Found desktop" = "Megtalált számítógép"; @@ -2871,6 +2878,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni."; + /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson lentebb a **Befejezés később** beállításra (az alkalmazás újraindításakor fel lesz ajánlva az adatbázis átköltöztetése)."; @@ -3042,7 +3052,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Érvénytelen átköltöztetési visszaigazolás"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Érvénytelen név!"; /* No comment provided by engineer. */ @@ -4128,7 +4138,7 @@ new chat action */ "Preserve the last message draft, with attachments." = "Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt."; /* No comment provided by engineer. */ -"Preset server address" = "Az előre beállított kiszolgáló címe"; +"Preset server address" = "Előre beállított kiszolgáló címe"; /* No comment provided by engineer. */ "Preset servers" = "Előre beállított kiszolgálók"; @@ -4236,7 +4246,7 @@ new chat action */ "Protect your chat profiles with a password!" = "Védje meg a csevegési profiljait egy jelszóval!"; /* No comment provided by engineer. */ -"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben."; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje az IP-címét a partnerei által kiválasztott üzenetváltási átjátszókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben."; /* No comment provided by engineer. */ "Protocol background timeout" = "Protokoll időtúllépése a háttérben"; @@ -4403,10 +4413,10 @@ swipe action */ "rejected call" = "elutasított hívás"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét."; +"Relay server is only used if necessary. Another party can observe your IP address." = "Az átjátszó csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét."; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát."; +"Relay server protects your IP address, but it can observe the duration of the call." = "Az átjátszó megvédi az IP-címét, de megfigyelheti a hívás időtartamát."; /* alert action */ "Remove" = "Eltávolítás"; @@ -5172,9 +5182,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "A SimpleX protokollokat a Trail of Bits auditálta."; -/* simplex link type */ -"SimpleX relay link" = "SimpleX továbbítókiszolgáló-hivatkozás"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Egyszerűsített inkognitómód"; @@ -5380,7 +5387,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Ideiglenes fájlhiba"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "A teszt a(z) %@ lépésnél sikertelen volt."; /* No comment provided by engineer. */ @@ -5924,7 +5932,7 @@ report reason */ "via one-time link" = "egy egyszer használható meghívón keresztül"; /* No comment provided by engineer. */ -"via relay" = "továbbítókiszolgálón keresztül"; +"via relay" = "átjátszón keresztül"; /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Biztonságos kvantumbiztos protokollon keresztül."; @@ -6071,7 +6079,7 @@ report reason */ "Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára."; /* alert message */ -"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@."; +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-átjátszók számára: %@."; /* No comment provided by engineer. */ "Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 3955f267ce..69045c9c23 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1248,7 +1248,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Confermato"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Connetti"; /* No comment provided by engineer. */ @@ -1353,6 +1354,9 @@ set passcode view */ /* chat list item title (it should not be shown */ "connection established" = "connessione stabilita"; +/* No comment provided by engineer. */ +"Connection failed" = "Connessione fallita"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "La connessione è bloccata dall'operatore del server:\n%@"; @@ -1464,7 +1468,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Angolo"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Correggere il nome a %@?"; /* No comment provided by engineer. */ @@ -2338,9 +2342,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Errore di apertura della chat"; -/* No comment provided by engineer. */ -"Error opening group" = "Errore di preparazione del gruppo"; - /* alert title */ "Error receiving file" = "Errore nella ricezione del file"; @@ -2454,7 +2455,8 @@ file error text snd error text */ "Error: %@" = "Errore: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Errore: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2504,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Esportazione archivio database…"; +/* No comment provided by engineer. */ +"failed" = "fallito"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Rimozione della password fallita"; @@ -2604,7 +2609,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "L'impronta digitale nell'indirizzo del server non corrisponde al certificato: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "L'impronta digitale nell'indirizzo del server non corrisponde al certificato."; /* No comment provided by engineer. */ @@ -2628,7 +2634,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Per tutti i moderatori"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Per il profilo di chat %@:"; /* No comment provided by engineer. */ @@ -2871,6 +2878,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Se inserisci il tuo codice di autodistruzione mentre apri l'app:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente."; + /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Se devi usare la chat adesso, tocca **Fallo più tardi** qui sotto (ti verrà offerto di migrare il database quando riavvii l'app)."; @@ -3042,7 +3052,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Conferma di migrazione non valida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Nome non valido!"; /* No comment provided by engineer. */ @@ -5172,9 +5182,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolli di SimpleX esaminati da Trail of Bits."; -/* simplex link type */ -"SimpleX relay link" = "Link del relay SimpleX"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Modalità incognito semplificata"; @@ -5380,7 +5387,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Errore del file temporaneo"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test fallito al passo %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 480eb39d36..38f90a2cad 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -890,7 +890,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm password" = "パスワードを確認"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "接続"; /* No comment provided by engineer. */ @@ -1720,7 +1721,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "チャットを素早く検索"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "サーバアドレスの証明証IDが正しくないかもしれません"; /* No comment provided by engineer. */ @@ -3133,7 +3135,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "テストはステップ %@ で失敗しました。"; /* No comment provided by engineer. */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 29f8bb5b3f..d4ace22665 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1209,7 +1209,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Bevestigd"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Verbind"; /* No comment provided by engineer. */ @@ -1416,7 +1417,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Hoek"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Juiste naam voor %@?"; /* No comment provided by engineer. */ @@ -2499,7 +2500,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Vind chats sneller"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; /* No comment provided by engineer. */ @@ -2523,7 +2525,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Voor alle moderators"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Voor chatprofiel %@:"; /* No comment provided by engineer. */ @@ -2928,7 +2931,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Ongeldige migratie bevestiging"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Ongeldige naam!"; /* No comment provided by engineer. */ @@ -5101,7 +5104,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Tijdelijke bestandsfout"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test mislukt bij stap %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index ed1f8850d8..7c28c9591c 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1248,7 +1248,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Potwierdzony"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Połącz"; /* No comment provided by engineer. */ @@ -1353,6 +1354,9 @@ set passcode view */ /* chat list item title (it should not be shown */ "connection established" = "połączenie ustanowione"; +/* No comment provided by engineer. */ +"Connection failed" = "Połączenie nie powiodło się"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Połączenie zostało zablokowane przez operatora serwera:\n%@"; @@ -1464,7 +1468,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Róg"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Poprawić imię na %@?"; /* No comment provided by engineer. */ @@ -2338,9 +2342,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Błąd otwierania czatu"; -/* No comment provided by engineer. */ -"Error opening group" = "Błąd otwierania grupy"; - /* alert title */ "Error receiving file" = "Błąd odbioru pliku"; @@ -2454,7 +2455,8 @@ file error text snd error text */ "Error: %@" = "Błąd: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Błąd: %@."; /* No comment provided by engineer. */ @@ -2502,6 +2504,9 @@ snd error text */ /* No comment provided by engineer. */ "Exporting database archive…" = "Eksportowanie archiwum bazy danych…"; +/* No comment provided by engineer. */ +"failed" = "nieudane"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "Nie udało się usunąć hasła"; @@ -2604,7 +2609,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "Odcisk palca w adresie serwera nie zgadza się z certyfikatem: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy."; /* No comment provided by engineer. */ @@ -2628,7 +2634,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Dla wszystkich moderatorów"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Dla profilu czatu %@:"; /* No comment provided by engineer. */ @@ -2871,6 +2878,9 @@ snd error text */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Jeśli wpiszesz swój pin samodestrukcji podczas otwierania aplikacji:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Jeśli dołączyłeś do kanałów lub je utworzyłeś, przestaną one działać na stałe."; + /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Jeśli potrzebujesz użyć czatu teraz, dotknij **Zrób to później** poniżej (zostanie Ci zaproponowana migracja bazy danych po ponownym uruchomieniu aplikacji)."; @@ -3042,7 +3052,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Nieprawidłowe potwierdzenie migracji"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Nieprawidłowa nazwa!"; /* No comment provided by engineer. */ @@ -5172,9 +5182,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protokoły SimpleX sprawdzone przez Trail of Bits."; -/* simplex link type */ -"SimpleX relay link" = "łącze przekaźnikowe SimpleX"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Uproszczony tryb incognito"; @@ -5380,7 +5387,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Tymczasowy błąd pliku"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test nie powiódł się na etapie %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 87a47ec2ab..eee33cc574 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1242,7 +1242,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Подтвержденный"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Соединиться"; /* No comment provided by engineer. */ @@ -1458,7 +1459,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Угол"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Исправить имя на %@?"; /* No comment provided by engineer. */ @@ -2326,9 +2327,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Ошибка при открытии чата"; -/* No comment provided by engineer. */ -"Error opening group" = "Ошибка при открытии группы"; - /* alert title */ "Error receiving file" = "Ошибка при получении файла"; @@ -2442,7 +2440,8 @@ file error text snd error text */ "Error: %@" = "Ошибка: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "Ошибка: %@."; /* No comment provided by engineer. */ @@ -2589,7 +2588,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "Хэш в адресе сервера не соответствует сертификату: %@."; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Возможно, хэш сертификата в адресе сервера неверный."; /* No comment provided by engineer. */ @@ -2613,7 +2613,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Для всех модераторов"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Для профиля чата %@:"; /* No comment provided by engineer. */ @@ -3024,7 +3025,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Ошибка подтверждения миграции"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Неверное имя!"; /* No comment provided by engineer. */ @@ -5127,9 +5128,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Аудит SimpleX протоколов от Trail of Bits."; -/* simplex link type */ -"SimpleX relay link" = "Ссылка SimpleX relay"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Упрощенный режим Инкогнито"; @@ -5335,7 +5333,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Временная ошибка файла"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Ошибка теста на шаге %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index d4b4dfd949..ffdd2006a6 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -626,7 +626,8 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm password" = "ยืนยันรหัสผ่าน"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "เชื่อมต่อ"; /* No comment provided by engineer. */ @@ -1381,7 +1382,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "ค้นหาแชทได้เร็วขึ้น"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; /* No comment provided by engineer. */ @@ -2770,7 +2772,8 @@ chat item action */ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "การทดสอบล้มเหลวในขั้นตอน %@"; /* No comment provided by engineer. */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 5cccb67170..30d83d16dd 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1242,7 +1242,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Onaylandı"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Bağlan"; /* No comment provided by engineer. */ @@ -1458,7 +1459,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Köşeleri yuvarlama"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "İsim %@ olarak düzeltilsin mi?"; /* No comment provided by engineer. */ @@ -2323,9 +2324,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Kişiyi hazırlama hatası"; -/* No comment provided by engineer. */ -"Error opening group" = "Grubu hazırlama hatası"; - /* alert title */ "Error receiving file" = "Dosya alınırken sorun oluştu"; @@ -2574,7 +2572,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Sohbetleri daha hızlı bul"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; /* No comment provided by engineer. */ @@ -2598,7 +2597,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Tüm moderatörler için"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Sohbet profili için %@:"; /* No comment provided by engineer. */ @@ -3009,7 +3009,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Geçersiz taşıma onayı"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Geçersiz isim!"; /* No comment provided by engineer. */ @@ -5109,9 +5109,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX protokolleri Trail of Bits tarafından incelenmiştir."; -/* simplex link type */ -"SimpleX relay link" = "SimpleX aktarıcı bağlantısı"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "Basitleştirilmiş gizli mod"; @@ -5317,7 +5314,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Geçici dosya hatası"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Test %@ adımında başarısız oldu."; /* No comment provided by engineer. */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 305e64fbcf..bf86d6a339 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1230,7 +1230,8 @@ set passcode view */ /* token status text */ "Confirmed" = "Підтверджений"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "Підключіться"; /* No comment provided by engineer. */ @@ -1443,7 +1444,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "Кут"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "Виправити ім'я на %@?"; /* No comment provided by engineer. */ @@ -2305,9 +2306,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Помилка відкриття чату"; -/* No comment provided by engineer. */ -"Error opening group" = "Помилка відкриття групи"; - /* alert title */ "Error receiving file" = "Помилка отримання файлу"; @@ -2550,7 +2548,8 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Швидше знаходьте чати"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "Відбиток в адресі сервера не співпадає з сертифікатом."; /* No comment provided by engineer. */ @@ -2574,7 +2573,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "Для всіх модераторів"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "Для профілю чату %@:"; /* No comment provided by engineer. */ @@ -2985,7 +2985,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "Недійсне підтвердження міграції"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "Неправильне ім'я!"; /* No comment provided by engineer. */ @@ -5257,7 +5257,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "Тимчасова помилка файлу"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "Тест завершився невдало на кроці %@."; /* No comment provided by engineer. */ diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index d5afea745d..43a0f665e4 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1245,7 +1245,8 @@ set passcode view */ /* token status text */ "Confirmed" = "已确定"; -/* server test step */ +/* relay test step +server test step */ "Connect" = "连接"; /* No comment provided by engineer. */ @@ -1461,7 +1462,7 @@ set passcode view */ /* No comment provided by engineer. */ "Corner" = "拐角"; -/* No comment provided by engineer. */ +/* alert message */ "Correct name to %@?" = "将名称更正为 %@?"; /* No comment provided by engineer. */ @@ -2329,9 +2330,6 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "打开聊天时出错"; -/* No comment provided by engineer. */ -"Error opening group" = "打开群时出错"; - /* alert title */ "Error receiving file" = "接收文件错误"; @@ -2445,7 +2443,8 @@ file error text snd error text */ "Error: %@" = "错误: %@"; -/* server test error */ +/* relay test error +server test error */ "Error: %@." = "错误:%@。"; /* No comment provided by engineer. */ @@ -2595,7 +2594,8 @@ snd error text */ /* No comment provided by engineer. */ "Fingerprint in server address does not match certificate: %@." = "服务器的指纹与证书不符:%@。"; -/* server test error */ +/* relay test error +server test error */ "Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; /* No comment provided by engineer. */ @@ -2619,7 +2619,8 @@ snd error text */ /* No comment provided by engineer. */ "For all moderators" = "所有 moderators"; -/* servers error */ +/* servers error +servers warning */ "For chat profile %@:" = "为聊天资料 %@:"; /* No comment provided by engineer. */ @@ -3033,7 +3034,7 @@ snd error text */ /* No comment provided by engineer. */ "Invalid migration confirmation" = "迁移确认无效"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid name!" = "无效名称!"; /* No comment provided by engineer. */ @@ -5151,9 +5152,6 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX 协议由 Trail of Bits 审阅。"; -/* simplex link type */ -"SimpleX relay link" = "SimpleX 中继链接"; - /* No comment provided by engineer. */ "Simplified incognito mode" = "简化的隐身模式"; @@ -5356,7 +5354,8 @@ report reason */ /* file error alert title */ "Temporary file error" = "临时文件错误"; -/* server test failure */ +/* relay test failure +server test failure */ "Test failed at step %@." = "在步骤 %@ 上测试失败。"; /* No comment provided by engineer. */ diff --git a/apps/multiplatform/.gitignore b/apps/multiplatform/.gitignore index 5d39eb29f2..bc00225c87 100644 --- a/apps/multiplatform/.gitignore +++ b/apps/multiplatform/.gitignore @@ -16,4 +16,7 @@ android/build android/release common/build desktop/build -release \ No newline at end of file +release + +# Generated SimpleX assets +common/src/commonMain/resources/assets/simplex/ \ No newline at end of file diff --git a/apps/multiplatform/build.gradle.kts b/apps/multiplatform/build.gradle.kts index a0cd35b3ca..64dd34afbf 100644 --- a/apps/multiplatform/build.gradle.kts +++ b/apps/multiplatform/build.gradle.kts @@ -25,6 +25,9 @@ buildscript { extra.set("application_id.suffix", prop["application_id.suffix"] ?: "") // Compression level for debug AND release apk. 0 = disable compression. Max is 9 extra.set("compression.level", (prop["compression.level"] as String?)?.toIntOrNull() ?: 0) + if (prop["simplex.assets.dir"] != null) { + extra.set("simplex.assets.dir", prop["simplex.assets.dir"]) + } // NOTE: If you need a different version of something, provide it in `local.properties` // like so: compose.version=123, or gradle.plugin.version=1.2.3, etc @@ -68,7 +71,6 @@ allprojects { repositories { google() mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://oss.sonatype.org/content/repositories/snapshots") maven("https://jitpack.io") } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index ea5579ed7d..65f0acd86c 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -11,6 +11,25 @@ plugins { group = "chat.simplex" version = extra["android.version_name"] as String +val simplexAssetsDir = rootProject.findProperty("simplex.assets.dir") as String? +val simplexAssetsLocal = file("src/commonMain/resources/assets/simplex") +val hasSimplexAssets = simplexAssetsDir != null + +if (simplexAssetsDir != null) { + val resolvedAssetsDir = rootProject.rootDir.resolve(simplexAssetsDir).absolutePath + tasks.register("copySimplexAssets") { + commandLine( + "${rootProject.rootDir}/../../scripts/android/copy-assets.sh", + resolvedAssetsDir, + simplexAssetsLocal.absolutePath + ) + } +} else { + tasks.register("cleanSimplexAssets") { + delete(simplexAssetsLocal) + } +} + kotlin { androidTarget() jvm("desktop") @@ -31,6 +50,11 @@ kotlin { } val commonMain by getting { + if (hasSimplexAssets) { + resources.srcDir(simplexAssetsLocal) + } else { + resources.srcDir("src/commonMain/resources/assets/default") + } dependencies { api(compose.runtime) api(compose.foundation) @@ -160,12 +184,18 @@ buildConfig { buildConfigField("int", "DESKTOP_VERSION_CODE", "${extra["desktop.version_code"]}") buildConfigField("String", "DATABASE_BACKEND", "\"${extra["database.backend"]}\"") buildConfigField("Boolean", "ANDROID_BUNDLE", "${extra["android.bundle"]}") + buildConfigField("Boolean", "SIMPLEX_ASSETS", "$hasSimplexAssets") } } afterEvaluate { tasks.named("generateMRcommonMain") { dependsOn("adjustFormatting") + if (hasSimplexAssets) { + dependsOn("copySimplexAssets") + } else { + dependsOn("cleanSimplexAssets") + } } tasks.create("adjustFormatting") { doLast { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index d9439a5474..e1696fe37b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -34,6 +34,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.ModalManager.Companion.fromEndToStartTransition import chat.simplex.common.views.helpers.ModalManager.Companion.fromStartToEndTransition import chat.simplex.common.views.localauth.VerticalDivider +import chat.simplex.common.views.newchat.* import chat.simplex.common.views.onboarding.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR @@ -383,7 +384,9 @@ fun CenterPartOfScreen() { } when (currentChatId.value) { null -> { - if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { + if (shouldShowOnboarding()) { + ConnectOnboardingView() + } else if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { Box( Modifier .fillMaxSize() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index f89a599bb4..bc81c958e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1157,10 +1157,10 @@ object ChatModel { showingInvitation.value = null chatsContext.chatItems.clearAndNotify() chatModel.chatId.value = withId + ModalManager.start.closeModals() + ModalManager.end.closeModals() } } - ModalManager.start.closeModals() - ModalManager.end.closeModals() } } @@ -1596,8 +1596,7 @@ sealed class ChatInfo: SomeChat, NamedChat { } } - val userCantSendReason: Pair? - get() { + fun userCantSendReason(allRelaysBroken: Boolean = false): Pair? { when (this) { is Direct -> { if (contact.sendMsgToConnect) return null @@ -1618,6 +1617,9 @@ sealed class ChatInfo: SomeChat, NamedChat { if (groupInfo.membership.memberActive) { when (groupChatScope) { null -> { + if (allRelaysBroken && groupInfo.useRelays) { + return generalGetString(MR.strings.cant_broadcast_message) to null + } if (groupInfo.membership.memberPending) { return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc) } @@ -1666,7 +1668,7 @@ sealed class ChatInfo: SomeChat, NamedChat { } } - val sendMsgEnabled get() = userCantSendReason == null + val sendMsgEnabled get() = userCantSendReason() == null val sndReady: Boolean get() = when(this) { @@ -2092,6 +2094,7 @@ data class GroupInfo ( ChatFeature.Calls -> false } override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null } + val isChannel: Boolean get() = groupProfile.isChannel override val displayName get() = localAlias.ifEmpty { groupProfile.displayName } override val fullName get() = groupProfile.fullName override val shortDescr get() = groupProfile.shortDescr @@ -2111,7 +2114,7 @@ data class GroupInfo ( val chatIconName: ImageResource get() = if (useRelays) { - MR.images.ic_bigtop_updates_padded + MR.images.ic_bigtop_updates_circle_filled } else when (businessChat?.chatType) { null -> MR.images.ic_supervised_user_circle_filled BusinessChatType.Business -> MR.images.ic_work_filled_padded @@ -2211,6 +2214,8 @@ data class GroupProfile ( val groupPreferences: GroupPreferences? = null, val memberAdmission: GroupMemberAdmission? = null ): NamedChat { + val isChannel: Boolean get() = publicGroup?.groupType == GroupType.Channel + companion object { val sampleData = GroupProfile( displayName = "team", @@ -2270,13 +2275,15 @@ enum class RelayStatus { @SerialName("new") RsNew, @SerialName("invited") RsInvited, @SerialName("accepted") RsAccepted, - @SerialName("active") RsActive; + @SerialName("active") RsActive, + @SerialName("inactive") RsInactive; val text: String get() = when (this) { RsNew -> generalGetString(MR.strings.relay_status_new) RsInvited -> generalGetString(MR.strings.relay_status_invited) RsAccepted -> generalGetString(MR.strings.relay_status_accepted) RsActive -> generalGetString(MR.strings.relay_status_active) + RsInactive -> generalGetString(MR.strings.relay_status_inactive) } } @@ -4303,7 +4310,7 @@ sealed class MsgContent { @Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCReport(override val text: String, val reason: ReportReason): MsgContent() - @Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() val isVoice: Boolean get() = @@ -4424,7 +4431,8 @@ object MsgContentSerializer : KSerializer { } "chat" -> { val chatLink = decoder.json.decodeFromString(json["chatLink"].toString()) - MsgContent.MCChat(text, chatLink) + val ownerSig = json["ownerSig"]?.let { decoder.json.decodeFromJsonElement(it) } + MsgContent.MCChat(text, chatLink, ownerSig) } else -> MsgContent.MCUnknown(t, text, json) } @@ -4485,6 +4493,7 @@ object MsgContentSerializer : KSerializer { put("type", "chat") put("text", value.text) put("chatLink", json.encodeToJsonElement(value.chatLink)) + value.ownerSig?.let { put("ownerSig", json.encodeToJsonElement(it)) } } is MsgContent.MCUnknown -> value.json } @@ -4492,16 +4501,51 @@ object MsgContentSerializer : KSerializer { } } -@Serializable -enum class MsgContentTag { - @SerialName("text") Text, - @SerialName("link") Link, - @SerialName("image") Image, - @SerialName("video") Video, - @SerialName("voice") Voice, - @SerialName("file") File, - @SerialName("report") Report, - @SerialName("chat") Chat, +@Serializable(with = MsgContentTagSerializer::class) +sealed class MsgContentTag { + @Serializable @SerialName("text") object Text: MsgContentTag() + @Serializable @SerialName("link") object Link: MsgContentTag() + @Serializable @SerialName("image") object Image: MsgContentTag() + @Serializable @SerialName("video") object Video: MsgContentTag() + @Serializable @SerialName("voice") object Voice: MsgContentTag() + @Serializable @SerialName("file") object File: MsgContentTag() + @Serializable @SerialName("report") object Report: MsgContentTag() + @Serializable @SerialName("chat") object Chat: MsgContentTag() + @Serializable @SerialName("unknown") data class Unknown(val type: String): MsgContentTag() + + val cmdString: String get() = when (this) { + is Text -> "text" + is Link -> "link" + is Image -> "image" + is Video -> "video" + is Voice -> "voice" + is File -> "file" + is Report -> "report" + is Chat -> "chat" + is Unknown -> type + } +} + +object MsgContentTagSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MsgContentTag", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): MsgContentTag = + when (val s = decoder.decodeString()) { + "text" -> MsgContentTag.Text + "link" -> MsgContentTag.Link + "image" -> MsgContentTag.Image + "video" -> MsgContentTag.Video + "voice" -> MsgContentTag.Voice + "file" -> MsgContentTag.File + "report" -> MsgContentTag.Report + "chat" -> MsgContentTag.Chat + "liveText" -> MsgContentTag.Text + else -> MsgContentTag.Unknown(s) + } + + override fun serialize(encoder: Encoder, value: MsgContentTag) { + encoder.encodeString(value.cmdString) + } } @Serializable @@ -4509,8 +4553,82 @@ sealed class MsgChatLink { @Serializable @SerialName("contact") data class Contact(val connLink: String, val profile: Profile, val business: Boolean) : MsgChatLink() @Serializable @SerialName("invitation") data class Invitation(val invLink: String, val profile: Profile) : MsgChatLink() @Serializable @SerialName("group") data class Group(val connLink: String, val groupProfile: GroupProfile) : MsgChatLink() + + val isPublicGroup: Boolean + get() = (this as? Group)?.groupProfile?.publicGroup != null + + val connLinkStr: String + get() = when (this) { + is Group -> connLink + is Contact -> connLink + is Invitation -> invLink + } + + val image: String? + get() = when (this) { + is Group -> groupProfile.image + is Contact -> profile.image + is Invitation -> profile.image + } + + val displayName: String + get() = when (this) { + is Group -> groupProfile.displayName + is Contact -> profile.displayName + is Invitation -> profile.displayName + } + + val fullName: String + get() = when (this) { + is Group -> groupProfile.fullName + is Contact -> profile.fullName + is Invitation -> profile.fullName + } + + val shortDescription: String? + get() { + val s = when (this) { + is Group -> groupProfile.shortDescr + is Contact -> profile.shortDescr + is Invitation -> profile.shortDescr + } + return s?.trim()?.ifEmpty { null } + } + + val iconRes: ImageResource + get() = when (this) { + is Group -> if (groupProfile.isChannel) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_supervised_user_circle_filled + is Contact -> if (business) MR.images.ic_work_filled_padded else MR.images.ic_account_circle_filled + is Invitation -> MR.images.ic_account_circle_filled + } + + val smallIconRes: ImageResource + get() = when (this) { + is Group -> if (groupProfile.isChannel) MR.images.ic_bigtop_updates else MR.images.ic_group + is Contact -> if (business) MR.images.ic_work else MR.images.ic_person + is Invitation -> MR.images.ic_person + } + + fun infoLine(signed: Boolean): String { + var s = when (this) { + is Group -> if (groupProfile.isChannel) generalGetString(MR.strings.chat_link_channel) else generalGetString(MR.strings.chat_link_group) + is Contact -> if (business) generalGetString(MR.strings.chat_link_business_address) else generalGetString(MR.strings.chat_link_contact_address) + is Invitation -> generalGetString(MR.strings.chat_link_one_time) + } + if (signed) { + s += " " + if (isPublicGroup) generalGetString(MR.strings.chat_link_from_owner) else generalGetString(MR.strings.chat_link_signed) + } + return s + } } +@Serializable +data class LinkOwnerSig( + val ownerId: String? = null, + val chatBinding: String, + val ownerSig: String +) + @Serializable class FormattedText(val text: String, val format: Format? = null) { val linkUri: String? get() = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 661b7e767f..f62d4c262d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -58,6 +58,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import java.util.concurrent.atomic.AtomicBoolean import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Date @@ -266,6 +267,7 @@ class AppPreferences { showReportsInSupportChatAlert to true, showDeleteConversationNotice to true, showDeleteContactNotice to true, + privacyLinkPreviewsShowAlert to true, ) private fun mkIntPreference(prefName: String, default: Int) = @@ -722,14 +724,20 @@ object ChatController { val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null if ((inProgress == null || inProgress.value) && alert != null) { return suspendCancellableCoroutine { cont -> + val resumed = AtomicBoolean(false) + fun safeResume(result: Result) { + if (resumed.compareAndSet(false, true)) { + cont.resumeWith(result) + } + } showRetryAlert( alert, onCancel = { - cont.resumeWith(Result.success(null)) + safeResume(Result.success(null)) }, onRetry = { withLongRunningApi { - cont.resumeWith( + safeResume( runCatching { coroutineScope { sendCmdWithRetry(rhId, cmd, inProgress = inProgress, retryNum = retryNum + 1) @@ -741,7 +749,7 @@ object ChatController { ) cont.invokeOnCancellation { - cont.resumeWith(Result.success(null)) + safeResume(Result.success(null)) } } } else { @@ -1043,7 +1051,7 @@ object ChatController { suspend fun apiGetChatContentTypes(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): List? { val r = sendCmd(rh, CC.ApiGetChatContentTypes(type, id, scope)) - if (r is API.Result && r.res is CR.ChatContentTypes) return r.res.contentTypes + if (r is API.Result && r.res is CR.ChatContentTypes) return r.res.contentTypes.filter { it !is MsgContentTag.Unknown } Log.e(TAG, "apiGetChatContentTypes bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_details), "${r.responseType}: ${r.details}") return null @@ -1135,6 +1143,13 @@ object ChatController { return processSendMessageCmd(rh, cmd)?.map { it.chatItem } } + suspend fun apiShareChatMsgContent(rh: Long?, shareChatType: ChatType, shareChatId: Long, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, sendAsGroup: Boolean): MsgContent? { + val r = sendCmd(rh, CC.ApiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup)) + if (r is API.Result && r.res is CR.ChatMsgContent) return r.res.msgContent + apiErrorAlert("apiShareChatMsgContent", generalGetString(MR.strings.error_sharing_channel), r) + return null + } + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List): CR.ForwardPlan? { val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds)) if (r is API.Result && r.res is CR.ForwardPlan) return r.res @@ -1485,9 +1500,9 @@ object ChatController { return null } - suspend fun apiConnectPlan(rh: Long?, connLink: String, inProgress: MutableState): Pair? { + suspend fun apiConnectPlan(rh: Long?, connLink: String, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState): Pair? { val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink), inProgress = inProgress) + val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, linkOwnerSig), inProgress = inProgress) if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan if (inProgress.value && r != null) apiConnectResponseAlert(r) return null @@ -1557,6 +1572,23 @@ object ChatController { } } + fun connErrorText(e: ChatError): String = when { + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.InvalidConnReq -> + generalGetString(MR.strings.invalid_connection_link) + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UnsupportedConnReq -> + generalGetString(MR.strings.unsupported_connection_link) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH -> + generalGetString(MR.strings.connection_error_auth) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.BLOCKED -> + "${generalGetString(MR.strings.connection_error_blocked)}: ${e.agentError.smpErr.blockInfo.reason.text}" + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.QUOTA -> + generalGetString(MR.strings.connection_reached_limit_of_undelivered_messages) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.BROKER -> + generalGetString(MR.strings.network_error) + else -> + "${generalGetString(MR.strings.error_prefix)}: ${e.string}" + } + suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? { val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData)) @@ -2111,10 +2143,16 @@ object ChatController { return null } - suspend fun apiNewPublicGroup(rh: Long?, incognito: Boolean, relayIds: List, groupProfile: GroupProfile): Triple>? { + sealed class PublicGroupCreationResult { + data class Created(val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List): PublicGroupCreationResult() + data class CreationFailed(val addRelayResults: List): PublicGroupCreationResult() + } + + suspend fun apiNewPublicGroup(rh: Long?, incognito: Boolean, relayIds: List, groupProfile: GroupProfile): PublicGroupCreationResult? { val userId = kotlin.runCatching { currentUserId("apiNewPublicGroup") }.getOrElse { return null } val r = sendCmdWithRetry(rh, CC.ApiNewPublicGroup(userId, incognito, relayIds, groupProfile)) - if (r is API.Result && r.res is CR.PublicGroupCreated) return Triple(r.res.groupInfo, r.res.groupLink, r.res.groupRelays) + if (r is API.Result && r.res is CR.PublicGroupCreated) return PublicGroupCreationResult.Created(r.res.groupInfo, r.res.groupLink, r.res.groupRelays) + if (r is API.Result && r.res is CR.PublicGroupCreationFailed) return PublicGroupCreationResult.CreationFailed(r.res.addRelayResults) if (r != null) throw Exception("${r.responseType}: ${r.details}") return null } @@ -3624,6 +3662,7 @@ sealed class CC { class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC() class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List): CC() class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List, val ttl: Int?): CC() + class ApiShareChatMsgContent(val shareChatType: ChatType, val shareChatId: Long, val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiNewPublicGroup(val userId: Long, val incognito: Boolean, val relayIds: List, val groupProfile: GroupProfile): CC() class ApiGetGroupRelays(val groupId: Long): CC() @@ -3683,7 +3722,7 @@ sealed class CC { class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() - class APIConnectPlan(val userId: Long, val connLink: String): CC() + class APIConnectPlan(val userId: Long, val connLink: String, val linkOwnerSig: LinkOwnerSig? = null): CC() class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC() class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData): CC() class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC() @@ -3790,7 +3829,7 @@ sealed class CC { val tag = if (contentTag == null) { "" } else { - " content=${contentTag.name.lowercase()}" + " content=${contentTag.cmdString}" } "/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") } @@ -3822,6 +3861,9 @@ sealed class CC { val ttlStr = if (ttl != null) "$ttl" else "default" "/_forward ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) " as_group=on" else ""} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}" } + is ApiShareChatMsgContent -> { + "/_share chat content ${chatRef(shareChatType, shareChatId, null)} ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" + } is ApiPlanForwardChatItems -> { "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}" } @@ -3884,7 +3926,10 @@ sealed class CC { is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" - is APIConnectPlan -> "/_connect plan $userId $connLink" + is APIConnectPlan -> { + val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else "" + "/_connect plan $userId $connLink$sigStr" + } is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}" is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} direct=${onOff(directLink)} ${json.encodeToString(groupShortLinkData)}" is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId" @@ -4003,6 +4048,7 @@ sealed class CC { is ApiChatItemReaction -> "apiChatItemReaction" is ApiGetReactionMembers -> "apiGetReactionMembers" is ApiForwardChatItems -> "apiForwardChatItems" + is ApiShareChatMsgContent -> "apiShareChatMsgContent" is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiNewPublicGroup -> "apiNewPublicGroup" @@ -4554,6 +4600,12 @@ data class RelayConnectionResult( val relayError: ChatError? = null ) +@Serializable +data class AddRelayResult( + val relay: UserChatRelay, + val relayError: ChatError? = null +) + @Serializable data class GroupShortLinkInfo( val direct: Boolean, @@ -6318,6 +6370,7 @@ sealed class CR { @Serializable @SerialName("subscriptionStatus") class SubscriptionStatusEvt(val subscriptionStatus: SubscriptionStatus, val connections: List): CR() @Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List): CR() + @Serializable @SerialName("chatMsgContent") class ChatMsgContent(val user: UserRef, val msgContent: MsgContent): CR() @Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR() @@ -6329,6 +6382,7 @@ sealed class CR { // group events @Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("publicGroupCreated") class PublicGroupCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List): CR() + @Serializable @SerialName("publicGroupCreationFailed") class PublicGroupCreationFailed(val user: UserRef, val addRelayResults: List): CR() @Serializable @SerialName("groupRelays") class GroupRelays(val user: UserRef, val groupInfo: GroupInfo, val groupRelays: List): CR() @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() @@ -6506,6 +6560,7 @@ sealed class CR { is SubscriptionStatusEvt -> "subscriptionStatus" is ChatInfoUpdated -> "chatInfoUpdated" is NewChatItems -> "newChatItems" + is ChatMsgContent -> "chatMsgContent" is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated" is ChatItemUpdated -> "chatItemUpdated" is ChatItemNotChanged -> "chatItemNotChanged" @@ -6516,6 +6571,7 @@ sealed class CR { is ForwardPlan -> "forwardPlan" is GroupCreated -> "groupCreated" is PublicGroupCreated -> "publicGroupCreated" + is PublicGroupCreationFailed -> "publicGroupCreationFailed" is GroupRelays -> "groupRelays" is SentGroupInvitation -> "sentGroupInvitation" is UserAcceptedGroupSent -> "userAcceptedGroupSent" @@ -6686,6 +6742,7 @@ sealed class CR { is SubscriptionStatusEvt -> "subscriptionStatus $subscriptionStatus\nconnections: $connections" is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo)) is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) + is ChatMsgContent -> withUser(user, msgContent.toString()) is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) @@ -6696,6 +6753,7 @@ sealed class CR { is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}") is GroupCreated -> withUser(user, json.encodeToString(groupInfo)) is PublicGroupCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays") + is PublicGroupCreationFailed -> withUser(user, "addRelayResults: $addRelayResults") is GroupRelays -> withUser(user, "groupInfo: $groupInfo\ngroupRelays: $groupRelays") is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member") is UserAcceptedGroupSent -> json.encodeToString(groupInfo) @@ -6840,6 +6898,12 @@ fun simplexChatLink(uri: String): String = if (uri.startsWith("simplex:/")) uri.replace("simplex:/", "https://simplex.chat/") else uri +@Serializable +sealed class OwnerVerification { + @Serializable @SerialName("verified") object Verified : OwnerVerification() + @Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification() +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @@ -6850,7 +6914,7 @@ sealed class ConnectionPlan { @Serializable sealed class InvitationLinkPlan { - @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): InvitationLinkPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): InvitationLinkPlan() @Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan() @Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan() @Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan() @@ -6858,7 +6922,7 @@ sealed class InvitationLinkPlan { @Serializable sealed class ContactAddressPlan { - @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): ContactAddressPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): ContactAddressPlan() @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() @@ -6868,11 +6932,12 @@ sealed class ContactAddressPlan { @Serializable sealed class GroupLinkPlan { - @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() + @Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null): GroupLinkPlan() @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() @Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan() + @Serializable @SerialName("noRelays") class NoRelays(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() } abstract class TerminalItem { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 88d9fbb705..cdd4140e3f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -42,7 +42,9 @@ expect fun desktopOpenDir(dir: File) fun createURIFromPath(absolutePath: String): URI = URI.create(URLEncoder.encode(absolutePath, "UTF-8")) -fun URI.toFile(): File = File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:")) +fun URI.toFile(): File = + if (scheme == "file") File(this) + else File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:")) fun copyFileToFile(from: File, to: URI, finally: () -> Unit) { try { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt index c50ea5c349..e3c86352d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt @@ -29,8 +29,8 @@ val GroupDark = Color(80, 80, 80, 60) val IncomingCallLight = Color(239, 237, 236, 255) val WarningOrange = Color(255, 127, 0, 255) val WarningYellow = Color(255, 192, 0, 255) -val FileLight = Color(183, 190, 199, 255) -val FileDark = Color(101, 101, 106, 255) +val FileLight = Color(191, 194, 199, 255) +val FileDark = Color(94, 94, 98, 255) val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.primaryVariant2 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 1b5a81a819..1de47df7ce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -626,6 +626,7 @@ val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp val DEFAULT_MIN_SECTION_ITEM_HEIGHT = 50.dp val DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL = 15.dp +val DEFAULT_WINDOW_WIDTH = 1366.dp val DEFAULT_START_MODAL_WIDTH = 388.dp val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp val DEFAULT_END_MODAL_WIDTH = 388.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 117b8955a1..114edeee3d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1953,7 +1953,7 @@ fun BoxScope.ChatItemsList( } false } - val swipeableModifier = if (appPlatform.isDesktop) Modifier else SwipeToDismissModifier( + val swipeableModifier = if (appPlatform.isDesktop || !chatInfo.sendMsgEnabled) Modifier else SwipeToDismissModifier( state = dismissState, directions = setOf(DismissDirection.EndToStart), swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, @@ -2339,7 +2339,7 @@ fun BoxScope.ChatItemsList( } val manager = LocalSelectionManager.current - val modifier = if (appPlatform.isDesktop && manager != null) SelectionHandler(manager, listState, mergedItems, linkMode) else Modifier + val modifier = if (appPlatform.isDesktop && manager != null) SelectionHandler(manager, listState, mergedItems, revealedItems, linkMode) else Modifier LazyColumnWithScrollBar( modifier.align(Alignment.BottomCenter), @@ -3204,7 +3204,7 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: ( val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) close?.invoke() ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null, isChannel = groupInfo.useRelays) + GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeChatLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeChatLinkView.kt new file mode 100644 index 0000000000..14edea3ed6 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeChatLinkView.kt @@ -0,0 +1,53 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.MsgChatLink +import chat.simplex.common.ui.theme.appColors +import chat.simplex.common.views.helpers.ProfileImage +import dev.icerock.moko.resources.compose.painterResource +import chat.simplex.res.MR + +@Composable +fun ComposeChatLinkView( + chatLink: MsgChatLink, + cancelEnabled: Boolean, + cancelPreview: () -> Unit +) { + val sentColor = MaterialTheme.appColors.sentMessage + Row( + Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background(sentColor) + .padding(start = 8.dp, top = 6.dp, bottom = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ProfileImage(size = 54.dp, image = chatLink.image, icon = chatLink.iconRes) + Column( + Modifier.fillMaxWidth().weight(1f).padding(horizontal = 8.dp) + ) { + Text(chatLink.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis) + chatLink.shortDescription?.let { descr -> + Text( + descr, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary, + ) + } + } + if (cancelEnabled) { + IconButton(onClick = cancelPreview) { + Icon(painterResource(MR.images.ic_close), null, tint = MaterialTheme.colors.primary) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 10f426b152..95f51bf284 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -1,6 +1,7 @@ @file:UseSerializers(UriSerializer::class, ComposeMessageSerializer::class) package chat.simplex.common.views.chat +import SectionItemView import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -19,6 +20,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -57,6 +60,7 @@ const val MAX_NUMBER_OF_MENTIONS = 3 sealed class ComposePreview { @Serializable object NoPreview: ComposePreview() @Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() + @Serializable class ChatLinkPreview(val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): ComposePreview() @Serializable class MediaPreview(val images: List, val content: List): ComposePreview() @Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview() @Serializable class FilePreview(val fileName: String, val uri: URI): ComposePreview() @@ -112,7 +116,12 @@ data class ComposeState( val mentions: MentionedMembers = emptyMap() ) { constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this( - ComposeMessage(editingItem.content.text), + ComposeMessage( + when (val mc = editingItem.content.msgContent) { + is MsgContent.MCChat -> stripTextLink(mc.text, mc.chatLink.connLinkStr) + else -> editingItem.content.text + } + ), editingItem.formattedText ?: FormattedText.plain(editingItem.content.text), liveMessage, chatItemPreview(editingItem), @@ -163,6 +172,7 @@ data class ComposeState( val hasContent = when (preview) { is ComposePreview.MediaPreview -> true is ComposePreview.VoicePreview -> true + is ComposePreview.ChatLinkPreview -> true is ComposePreview.FilePreview -> true else -> !whitespaceOnly || forwarding || liveMessage != null || submittingValidReport } @@ -174,6 +184,7 @@ data class ComposeState( val linkPreviewAllowed: Boolean get() = when (preview) { + is ComposePreview.ChatLinkPreview -> false is ComposePreview.MediaPreview -> false is ComposePreview.VoicePreview -> false is ComposePreview.FilePreview -> false @@ -200,6 +211,7 @@ data class ComposeState( get() = when (preview) { ComposePreview.NoPreview -> false is ComposePreview.CLinkPreview -> false + is ComposePreview.ChatLinkPreview -> false is ComposePreview.MediaPreview -> preview.content.isNotEmpty() is ComposePreview.VoicePreview -> false is ComposePreview.FilePreview -> true @@ -390,20 +402,46 @@ fun ComposeView( val recState: MutableState = remember { mutableStateOf(RecordingState.NotStarted) } AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } } + suspend fun fetchAndUpdateLinkPreview(url: String) { + composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) + val lp = getLinkPreview(url) + if (lp != null && pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) + pendingLinkUrl.value = null + } else if (pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) + pendingLinkUrl.value = null + } + } + fun loadLinkPreview(url: String, wait: Long? = null) { if (pendingLinkUrl.value == url) { - composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) withLongRunningApi(slow = 60_000) { if (wait != null) delay(wait) - val lp = getLinkPreview(url) - if (lp != null && pendingLinkUrl.value == url) { - chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) // to avoid showing alert to current users, show alert in v6.5 - composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) - pendingLinkUrl.value = null - } else if (pendingLinkUrl.value == url) { - composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) - pendingLinkUrl.value = null + if (pendingLinkUrl.value != url) return@withLongRunningApi + if (chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.get() + && !chatModel.controller.appPrefs.networkUseSocksProxy.get()) { + showLinkPreviewsConfirmAlert { enable -> + if (enable != null) { + chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) + chatModel.controller.appPrefs.privacyLinkPreviews.set(enable) + if (enable) { + withLongRunningApi(slow = 60_000) { fetchAndUpdateLinkPreview(url) } + } else if (pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) + pendingLinkUrl.value = null + } + } else { + cancelledLinks.add(url) + if (pendingLinkUrl.value == url) { + composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) + pendingLinkUrl.value = null + } + } + } + return@withLongRunningApi } + fetchAndUpdateLinkPreview(url) } } } @@ -468,6 +506,7 @@ fun ComposeView( is SharedContent.File -> listOf(shared.uri.toString()) is SharedContent.Text -> emptyList() is SharedContent.Forward -> emptyList() + is SharedContent.ChatLink -> emptyList() } // When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing chatModel.filesToDelete.removeAll { file -> @@ -672,8 +711,11 @@ fun ComposeView( is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration) is MsgContent.MCFile -> MsgContent.MCFile(msgText) is MsgContent.MCReport -> MsgContent.MCReport(msgText, reason = msgContent.reason) - // TODO [short links] update chat link - is MsgContent.MCChat -> MsgContent.MCChat(msgText, chatLink = msgContent.chatLink) + is MsgContent.MCChat -> { + val linkStr = msgContent.chatLink.connLinkStr + val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr" + MsgContent.MCChat(text, chatLink = msgContent.chatLink, ownerSig = msgContent.ownerSig) + } is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json) } } @@ -760,6 +802,11 @@ fun ComposeView( when (val preview = cs.preview) { ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText)) is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) + is ComposePreview.ChatLinkPreview -> { + val linkStr = preview.chatLink.connLinkStr + val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr" + msgs.add(MsgContent.MCChat(text, preview.chatLink, preview.ownerSig)) + } is ComposePreview.MediaPreview -> { // TODO batch send: batch media previews preview.content.forEachIndexed { index, it -> @@ -1060,6 +1107,11 @@ fun ComposeView( ::cancelLinkPreview, cancelEnabled = !composeState.value.inProgress ) + is ComposePreview.ChatLinkPreview -> ComposeChatLinkView( + chatLink = preview.chatLink, + cancelEnabled = !composeState.value.inProgress, + cancelPreview = { composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) } + ) is ComposePreview.MediaPreview -> ComposeImageView( preview, ::cancelImages, @@ -1128,8 +1180,10 @@ fun ComposeView( } } - val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) - val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason) + val ownerRelayState = ownerRelayState(chat, chatModel) + + val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason(ownerRelayState?.noActiveRelays == true)) + val sendMsgEnabled = rememberUpdatedState(userCantSendReason.value == null) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) @Composable @@ -1250,6 +1304,8 @@ fun ComposeView( composeState.value = cs.copy(inProgress = false, progressByTimeout = false) } else if (!cs.empty) { if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { + recState.value = RecordingState.NotStarted + RecorderInterface.stopRecording?.invoke() composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) } if (saveLastDraft) { @@ -1307,7 +1363,7 @@ fun ComposeView( sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, - placeholder = placeholder ?: composeState.value.placeholder, + placeholder = if (userCantSendReason.value != null) "" else placeholder ?: composeState.value.placeholder, sendMessage = { ttl -> sendMessage(ttl) resetLinkPreview() @@ -1438,6 +1494,22 @@ fun ComposeView( contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo), preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview ) + is SharedContent.ChatLink -> { + val cInfo = chat.chatInfo + val sendAsGroup = (cInfo as? ChatInfo.Group)?.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false + withBGApi { + val mc = chatModel.controller.apiShareChatMsgContent( + chat.remoteHostId, ChatType.Group, shared.groupInfo.groupId, + cInfo.chatType, cInfo.apiId, + cInfo.groupChatScope(), sendAsGroup + ) + if (mc is MsgContent.MCChat) { + composeState.value = composeState.value.copy( + preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig) + ) + } + } + } null -> {} } chatModel.sharedContent.value = null @@ -1470,11 +1542,10 @@ fun ComposeView( && gInfo.membership.memberStatus !in listOf(GroupMemberStatus.MemRejected, GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) ) { if (gInfo.membership.memberRole == GroupMemberRole.Owner) { - val relays = if (ChannelRelaysModel.groupId.value == gInfo.groupId) ChannelRelaysModel.groupRelays.toList() else emptyList() - val failedCount = relays.count { relayMemberConnFailed(chatModel, it) != null } - val activeCount = relays.count { it.relayStatus == RelayStatus.RsActive && relayMemberConnFailed(chatModel, it) == null } - if (relays.isNotEmpty() && activeCount < relays.size) { - OwnerChannelRelayBar(chatModel, relays, activeCount, failedCount, relayListExpanded) + ownerRelayState?.let { s -> + if (s.activeCount < s.relays.size) { + OwnerChannelRelayBar(chatModel, s.relays, s.activeCount, s.failedCount, s.removedCount, relayListExpanded) + } } } else { val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted() @@ -1482,14 +1553,13 @@ fun ComposeView( .filter { it.memberRole == GroupMemberRole.Relay } .sortedBy { hostFromRelayLink(it.relayLink ?: "") } val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress - val connectedCount = relayMembers.count { it.activeConn?.connStatus == ConnStatus.Ready } - val deletedCount = relayMembers.count { it.activeConn?.connStatus == ConnStatus.Deleted } - val failedCount = relayMembers.count { it.activeConn?.connFailedErr != null } - val errorCount = deletedCount + failedCount - val resolvedCount = connectedCount + deletedCount + val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) } + val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null } + val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null } + val resolvedCount = connectedCount + removedCount + failedCount val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size - if (total > 0 && (!showProgress || resolvedCount < total)) { - SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, errorCount, total, showProgress, relayListExpanded) + if (total > 0 && (removedCount + failedCount > 0 || resolvedCount < total)) { + SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, showProgress, relayListExpanded) } } } @@ -1633,31 +1703,102 @@ fun ComposeView( } } +private fun showLinkPreviewsConfirmAlert(onChoice: (Boolean?) -> Unit) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.link_previews_alert_title), + text = AnnotatedString(generalGetString(MR.strings.link_previews_alert_desc)), + onDismissRequest = { onChoice(null) }, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + onChoice(false) + }) { + Text(stringResource(MR.strings.link_previews_alert_disable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + onChoice(true) + }) { + Text(stringResource(MR.strings.link_previews_alert_enable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } +// SectionItemView({ +// AlertManager.shared.hideAlert() +// onChoice(null) +// }) { +// Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground) +// } + } + } + ) +} + @Composable private fun OwnerChannelRelayBar( chatModel: ChatModel, relays: List, activeCount: Int, failedCount: Int, + removedCount: Int, relayListExpanded: MutableState ) { val total = relays.size - val sorted = relays.sortedBy { relayDisplayName(it) } + val allBroken = activeCount == 0 && (failedCount + removedCount) == total + val members = chatModel.groupMembers.value.associateBy { it.groupMemberId } + val sorted = relays.map { relay -> relay to members[relay.groupMemberId] }.sortedBy { relayDisplayName(it.first) } Column(Modifier.background(MaterialTheme.colors.surface)) { RelayBarHeader(relayListExpanded) { - if (activeCount + failedCount < total) { + if (!allBroken && activeCount + failedCount + removedCount < total) { RelayProgressIndicator(active = activeCount, total = total) } - val statusText = if (failedCount > 0) { - String.format(generalGetString(MR.strings.relay_bar_active_with_failures), activeCount, total, failedCount) + if (allBroken) { + val statusText = if (removedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_removed) + } else if (failedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_failed) + } else { + generalGetString(MR.strings.relay_bar_no_active_relays) + } + Text(statusText, color = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_warning), + contentDescription = null, + tint = WarningOrange, + modifier = Modifier.size(18.dp) + ) + } else if (activeCount + failedCount + removedCount >= total) { + val statusText = if (failedCount > 0 && removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_not_active), failedCount + removedCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_failed), failedCount) + } else { + String.format(generalGetString(MR.strings.relay_bar_relays_removed), removedCount) + } + Text(statusText, color = MaterialTheme.colors.secondary) } else { - String.format(generalGetString(MR.strings.relay_bar_active), activeCount, total) + val statusText = if (failedCount > 0 && removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_active_with_errors), activeCount, total, failedCount + removedCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_active_with_failures), activeCount, total, failedCount) + } else if (removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_active_with_removed), activeCount, total, removedCount) + } else { + String.format(generalGetString(MR.strings.relay_bar_active), activeCount, total) + } + Text(statusText, color = MaterialTheme.colors.secondary) } - Text(statusText, modifier = Modifier.weight(1f), color = MaterialTheme.colors.secondary) } if (relayListExpanded.value) { - sorted.forEach { relay -> - val failedErr = relayMemberConnFailed(chatModel, relay) + if (allBroken) { + Text( + generalGetString(MR.strings.relay_bar_owner_no_delivery), + modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = DEFAULT_PADDING, bottom = 4.dp), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp + ) + } + sorted.forEach { (relay, m) -> + val failedErr = m?.activeConn?.connFailedErr RelayBarDetailRow( onClick = if (failedErr != null) { { @@ -1674,7 +1815,7 @@ private fun OwnerChannelRelayBar( fontSize = 12.sp ) Spacer(Modifier.weight(1f)) - RelayStatusIndicator(relay.relayStatus, connFailed = failedErr != null) + RelayStatusIndicator(relay.relayStatus, connFailed = failedErr != null, memberStatus = m?.memberStatus) } } } @@ -1686,28 +1827,65 @@ private fun SubscriberChannelRelayBar( hostnames: List, relayMembers: List, connectedCount: Int, - errorCount: Int, + removedCount: Int, + failedCount: Int, total: Int, showProgress: Boolean, relayListExpanded: MutableState ) { + val errorCount = removedCount + failedCount + val allBroken = connectedCount == 0 && errorCount == total Column(Modifier.background(MaterialTheme.colors.surface)) { RelayBarHeader(relayListExpanded) { - if (showProgress && connectedCount + errorCount < total) { - RelayProgressIndicator(active = connectedCount, total = total) - } - val statusText = if (showProgress) { - if (errorCount > 0) { + if (allBroken) { + val statusText = if (removedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_removed) + } else if (failedCount == total) { + generalGetString(MR.strings.relay_bar_all_relays_failed) + } else { + generalGetString(MR.strings.relay_bar_no_active_relays) + } + Text(statusText, color = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_warning), + contentDescription = null, + tint = WarningOrange, + modifier = Modifier.size(18.dp) + ) + } else if (connectedCount + removedCount + failedCount >= total && errorCount > 0) { + val statusText = if (failedCount > 0 && removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_not_active), failedCount + removedCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_relays_failed), failedCount) + } else { + String.format(generalGetString(MR.strings.relay_bar_relays_removed), removedCount) + } + Text(statusText, color = MaterialTheme.colors.secondary) + } else { + if (showProgress && connectedCount + errorCount < total) { + RelayProgressIndicator(active = connectedCount, total = total) + } + val statusText = if (failedCount > 0 && removedCount > 0) { String.format(generalGetString(MR.strings.relay_bar_connected_with_errors), connectedCount, total, errorCount) + } else if (failedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_connected_with_failures), connectedCount, total, failedCount) + } else if (removedCount > 0) { + String.format(generalGetString(MR.strings.relay_bar_connected_with_removed), connectedCount, total, removedCount) } else { String.format(generalGetString(MR.strings.relay_bar_connected), connectedCount, total) } - } else { - String.format(generalGetString(MR.strings.relay_bar_count), total) + Text(statusText, color = MaterialTheme.colors.secondary) } - Text(statusText, modifier = Modifier.weight(1f), color = MaterialTheme.colors.secondary) } if (relayListExpanded.value) { + if (allBroken) { + Text( + generalGetString(MR.strings.relay_bar_subscriber_waiting), + modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = DEFAULT_PADDING, bottom = 4.dp), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp + ) + } if (relayMembers.isEmpty()) { hostnames.forEach { relay -> RelayBarDetailRow { @@ -1775,6 +1953,7 @@ private fun RelayBarHeader( verticalAlignment = Alignment.CenterVertically ) { content() + Spacer(Modifier.weight(1f)) Icon( painterResource(if (expanded.value) MR.images.ic_chevron_down else MR.images.ic_chevron_up), contentDescription = null, @@ -1800,9 +1979,31 @@ private fun RelayBarDetailRow( } } -private fun relayMemberConnFailed(chatModel: ChatModel, relay: GroupRelay): String? { - return chatModel.groupMembers.value - .firstOrNull { it.groupMemberId == relay.groupMemberId } - ?.activeConn?.connFailedErr +private fun ownerRelayState(chat: Chat, chatModel: ChatModel): OwnerRelayState? { + val gInfo = (chat.chatInfo as? ChatInfo.Group)?.groupInfo ?: return null + if (!gInfo.useRelays || gInfo.membership.memberRole != GroupMemberRole.Owner || + gInfo.membership.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + ) return null + val relays = if (ChannelRelaysModel.groupId.value == gInfo.groupId) ChannelRelaysModel.groupRelays.toList() else emptyList() + if (relays.isEmpty()) return null + val relayMembers = relays.map { relay -> + relay to chatModel.groupMembers.value.firstOrNull { it.groupMemberId == relay.groupMemberId } + } + val removedCount = relayMembers.count { (_, m) -> relayMemberRemoved(m?.memberStatus) } + val activeCount = relayMembers.count { (relay, m) -> !relayMemberRemoved(m?.memberStatus) && relay.relayStatus == RelayStatus.RsActive && m?.activeConn?.connFailedErr == null } + val failedCount = relayMembers.count { (_, m) -> !relayMemberRemoved(m?.memberStatus) && m?.activeConn?.connFailedErr != null } + val noActiveRelays = activeCount == 0 && (failedCount + removedCount) == relays.size + return OwnerRelayState(relays, activeCount, failedCount, removedCount, noActiveRelays) } +private data class OwnerRelayState( + val relays: List, + val activeCount: Int, + val failedCount: Int, + val removedCount: Int, + val noActiveRelays: Boolean +) + +private fun relayMemberRemoved(status: GroupMemberStatus?): Boolean = + status in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt index 1501fb7938..e681c4fed7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt @@ -39,7 +39,7 @@ fun ContextItemView( val receivedColor = MaterialTheme.appColors.receivedMessage @Composable - fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) { + fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int, prefix: AnnotatedString? = null, stripLink: String? = null) { val inlineContent: Pair Unit, Map>? = if (attachment != null) { remember(contextItem.id) { val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = { @@ -68,24 +68,35 @@ fun ContextItemView( userMemberId = when { chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId else -> null - } + }, + prefix = prefix, + stripLink = stripLink, ) } fun attachment(contextItem: ChatItem): ImageResource? { val fileIsLoaded = getLoadedFilePath(contextItem.file) != null - return when (contextItem.content.msgContent) { + val mc = contextItem.content.msgContent + return when (mc) { is MsgContent.MCFile -> if (fileIsLoaded) MR.images.ic_draft_filled else null is MsgContent.MCImage -> MR.images.ic_image is MsgContent.MCVoice -> if (fileIsLoaded) MR.images.ic_play_arrow_filled else null + is MsgContent.MCChat -> mc.chatLink.smallIconRes else -> null } } @Composable fun ContextMsgPreview(contextItem: ChatItem, lines: Int) { - MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines) + val mc = contextItem.content.msgContent + if (mc is MsgContent.MCChat) { + val hasText = contextItem.text != mc.chatLink.connLinkStr + val prefix = buildAnnotatedString { append(mc.chatLink.displayName + if (hasText) " - " else "") } + MessageText(contextItem, remember(contextItem.id) { mc.chatLink.smallIconRes }, lines, prefix = prefix, stripLink = mc.chatLink.connLinkStr) + } else { + MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines) + } } val sent = contextItems[0].chatDir.sent diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 9184071c07..0948551c7e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -195,7 +195,7 @@ fun SendMsgView( ) } } - if (timedMessageAllowed) { + if (timedMessageAllowed && !cs.editing) { menuItems.add { ItemAction( generalGetString(MR.strings.disappearing_message), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt index 626aea0806..dee39dde7c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/TextSelection.kt @@ -196,12 +196,13 @@ class SelectionManager { } } - fun getSelectedCopiedText(items: List, linkMode: SimplexLinkMode): String { + fun getSelectedCopiedText(items: List, revealedItems: Set, linkMode: SimplexLinkMode): String { val r = range ?: return "" val lo = minOf(r.startIndex, r.endIndex) val hi = maxOf(r.startIndex, r.endIndex) return (lo..hi).mapNotNull { idx -> val ci = items.getOrNull(idx)?.newest()?.item ?: return@mapNotNull null + if (ci.meta.itemDeleted != null && (!revealedItems.contains(ci.id) || ci.isDeletedContent)) return@mapNotNull null val sel = selectedRange(range, idx) ?: return@mapNotNull null selectedItemCopiedText(ci, sel, linkMode) }.reversed().joinToString("\n") @@ -291,6 +292,7 @@ fun BoxScope.SelectionHandler( manager: SelectionManager, listState: State, mergedItems: State, + revealedItems: State>, linkMode: SimplexLinkMode ): Modifier { val touchSlop = LocalViewConfiguration.current.touchSlop @@ -311,7 +313,7 @@ fun BoxScope.SelectionHandler( manager.listState = listState manager.onCopySelection = { - clipboard.setText(AnnotatedString(manager.getSelectedCopiedText(mergedItems.value.items, linkMode))) + clipboard.setText(AnnotatedString(manager.getSelectedCopiedText(mergedItems.value.items, revealedItems.value, linkMode))) showToast(generalGetString(MR.strings.copied)) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt index e8f2a36fff..39ebe5afd8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt @@ -131,7 +131,9 @@ private fun subscriberRelayStatusText(member: GroupMember): String { } private fun ownerRelayStatusText(member: GroupMember, groupRelays: List): String { - return if (member.activeConn?.connStatus is ConnStatus.Failed) { + return if (member.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)) { + relayConnStatus(member).first + } else if (member.activeConn?.connStatus is ConnStatus.Failed) { generalGetString(MR.strings.relay_conn_status_failed) } else if (member.activeConn?.connDisabled == true) { generalGetString(MR.strings.member_info_member_disabled) @@ -144,6 +146,11 @@ private fun ownerRelayStatusText(member: GroupMember, groupRelays: List { + when (member.memberStatus) { + GroupMemberStatus.MemLeft -> return generalGetString(MR.strings.relay_conn_status_removed_by_operator) to Color.Red + GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted -> return member.memberStatus.text to Color.Red + else -> {} + } return when (member.activeConn?.connStatus) { is ConnStatus.Ready -> generalGetString(MR.strings.relay_conn_status_connected) to Color.Green is ConnStatus.Deleted -> generalGetString(MR.strings.relay_conn_status_deleted) to Color.Red diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 78eb31ccbe..156ad2cd97 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -167,7 +167,7 @@ fun ModalData.GroupChatInfoView( clearChat = { clearChatDialog(chat, close) }, leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { - ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays) } + ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) } }, onSearchClicked = onSearchClicked, deletingItems = deletingItems @@ -554,6 +554,11 @@ fun ModalData.GroupChatInfoLayout( } else if (channelLink != null) { anyTopSectionRowShow = true ChannelLinkQRCodeSection(channelLink) + ShareViaChatButton { + chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo) + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + } } if (groupInfo.isOwner || activeSortedMembers.any { it.memberRole >= GroupMemberRole.Owner }) { anyTopSectionRowShow = true @@ -594,32 +599,27 @@ fun ModalData.GroupChatInfoLayout( } } } - val showEditSection = (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) - || groupInfo.groupProfile.description != null - || !groupInfo.useRelays if (anyTopSectionRowShow) { SectionDividerSpaced(maxBottomPadding = false) } - if (showEditSection) { - SectionView { - if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { - val editProfileTitleId = if (groupInfo.useRelays) MR.strings.button_edit_channel_profile else MR.strings.button_edit_group_profile - EditGroupProfileButton(editProfileTitleId, editGroupProfile) - } - if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) { - AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) - } - if (!groupInfo.useRelays) { - val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences - GroupPreferencesButton(prefsTitleId, openPreferences) - } + SectionView { + if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { + val editProfileTitleId = if (groupInfo.useRelays) MR.strings.button_edit_channel_profile else MR.strings.button_edit_group_profile + EditGroupProfileButton(editProfileTitleId, editGroupProfile) } - if (!groupInfo.useRelays) { - val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs - SectionTextFooter(stringResource(footerId)) + if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) { + AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + val prefsTitleId = if (groupInfo.useRelays) MR.strings.channel_preferences + else if (groupInfo.businessChat == null) MR.strings.group_preferences + else MR.strings.chat_preferences + GroupPreferencesButton(prefsTitleId, openPreferences) } + val footerId = if (groupInfo.useRelays) MR.strings.only_channel_owners_can_change_prefs + else if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs + else MR.strings.only_chat_owners_can_change_prefs + SectionTextFooter(stringResource(footerId)) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionView { if (!groupInfo.useRelays) { @@ -1138,6 +1138,15 @@ private fun ChannelLinkQRCodeSection(groupLink: String) { } } +@Composable +private fun ShareViaChatButton(onClick: () -> Unit) { + SectionItemView(onClick) { + Icon(painterResource(MR.images.ic_forward), null, tint = MaterialTheme.colors.primary) + Spacer(Modifier.width(8.dp)) + Text(stringResource(MR.strings.share_via_chat), color = MaterialTheme.colors.primary) + } +} + @Composable private fun ChannelMembersButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) { val title = if (groupInfo.isOwner) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index c9745359b9..c0b107b5dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -33,6 +33,7 @@ fun GroupLinkView( onGroupLinkUpdated: ((GroupLink?) -> Unit)?, creatingGroup: Boolean = false, isChannel: Boolean = false, + shareGroupInfo: GroupInfo? = null, close: (() -> Unit)? = null ) { var groupLinkVar by rememberSaveable(stateSaver = GroupLink.nullableStateSaver) { mutableStateOf(groupLink) } @@ -124,6 +125,7 @@ fun GroupLinkView( groupLinkMemberRole, creatingLink, isChannel = isChannel, + shareGroupInfo = shareGroupInfo, createLink = ::createLink, showAddShortLinkAlert = ::showAddShortLinkAlert, updateLink = { @@ -171,6 +173,7 @@ fun GroupLinkLayout( groupLinkMemberRole: MutableState, creatingLink: Boolean, isChannel: Boolean = false, + shareGroupInfo: GroupInfo? = null, createLink: () -> Unit, showAddShortLinkAlert: ((() -> Unit)?) -> Unit, updateLink: () -> Unit, @@ -230,40 +233,56 @@ fun GroupLinkLayout( } else null) { SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value) } - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp) - ) { - val clipboard = LocalClipboardManager.current - SimpleButton( - stringResource(MR.strings.share_link), - icon = painterResource(MR.images.ic_share), - click = { - if (!isChannel && groupLink.shouldBeUpgraded) { - showAddShortLinkAlert { - clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) - } - } else { + if (!isChannel && groupLink.shouldBeUpgraded) { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.upgrade_group_link), + click = { showAddShortLinkAlert(null) }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) + } + val clipboard = LocalClipboardManager.current + SettingsActionItem( + painterResource(MR.images.ic_share), + stringResource(MR.strings.share_link), + click = { + if (!isChannel && groupLink.shouldBeUpgraded) { + showAddShortLinkAlert { clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) } + } else { + clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) } + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) + if (shareGroupInfo != null) { + SettingsActionItem( + painterResource(MR.images.ic_forward), + stringResource(MR.strings.share_via_chat), + click = { + chatModel.sharedContent.value = SharedContent.ChatLink(shareGroupInfo) + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, ) - if (creatingGroup && close != null) { - ContinueButton(close) - } else if (!isChannel) { - SimpleButton( - stringResource(MR.strings.delete_link), - icon = painterResource(MR.images.ic_delete), - color = Color.Red, - click = deleteLink - ) - } } - if (!isChannel && groupLink.shouldBeUpgraded) { - AddShortLinkButton(text = stringResource(MR.strings.upgrade_group_link)) { - showAddShortLinkAlert(null) - } + if (!creatingGroup && !isChannel) { + SettingsActionItem( + painterResource(MR.images.ic_delete), + stringResource(MR.strings.delete_link), + click = deleteLink, + iconColor = Color.Red, + textColor = Color.Red, + ) + } + if (creatingGroup && close != null) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + ContinueButton(close) } } } @@ -271,17 +290,6 @@ fun GroupLinkLayout( } } -@Composable -private fun AddShortLinkButton(text: String, onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_add), - text, - onClick, - iconColor = MaterialTheme.colors.primary, - textColor = MaterialTheme.colors.primary, - ) -} - @Composable private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState, enabled: Boolean = true) { Row( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index ddf0456822..902b4c9828 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -10,6 +10,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -56,10 +57,12 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> afterSave() } } + val saveTextId = if (gInfo.useRelays) MR.strings.save_and_notify_channel_subscribers + else MR.strings.save_and_notify_group_members ModalView( close = { if (preferences == currentPreferences) close() - else showUnsavedChangesAlert({ savePrefs(close) }, close) + else showUnsavedChangesAlert({ savePrefs(close) }, close, saveTextId) }, ) { GroupPreferencesLayout( @@ -97,17 +100,11 @@ private fun GroupPreferencesLayout( savePrefs: () -> Unit, openMemberAdmission: () -> Unit, ) { - ColumnWithScrollBar { - val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences - AppBarTitle(stringResource(titleId)) - if (groupInfo.businessChat == null) { - MemberAdmissionButton(openMemberAdmission) - SectionDividerSpaced(maxBottomPadding = false) - } + val onTTLUpdated = { ttl: Int? -> + applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) + } + @Composable fun TimedMessagesPreference() { val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } - val onTTLUpdated = { ttl: Int? -> - applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) - } FeatureSection(GroupFeature.TimedMessages, timedMessages, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> if (enable == GroupFeatureEnabled.ON) { applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400))) @@ -115,58 +112,104 @@ private fun GroupPreferencesLayout( applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl))) } } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun DirectMessagesPreference() { val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) } val directMessagesRole = remember(preferences) { mutableStateOf(preferences.directMessages.role) } FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, directMessagesRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(directMessages = RoleGroupPreference(enable = enable, role))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun FullDeletePreference() { val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) } FeatureSection(GroupFeature.FullDelete, allowFullDeletion, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = enable))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun ReactionsPreference() { val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) } FeatureSection(GroupFeature.Reactions, allowReactions, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(reactions = GroupPreference(enable = enable))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun VoicePreference() { val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) } val voiceRole = remember(preferences) { mutableStateOf(preferences.voice.role) } FeatureSection(GroupFeature.Voice, allowVoice, voiceRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(voice = RoleGroupPreference(enable = enable, role))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun FilesPreference() { val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) } val filesRole = remember(preferences) { mutableStateOf(preferences.files.role) } FeatureSection(GroupFeature.Files, allowFiles, filesRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role))) } - - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun SimplexLinksPreference() { val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) } val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) } FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role -> applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role))) } - - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun ReportsPreference() { val enableReports = remember(preferences) { mutableStateOf(preferences.reports.enable) } FeatureSection(GroupFeature.Reports, enableReports, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(reports = GroupPreference(enable = enable))) } - SectionDividerSpaced(true, maxBottomPadding = false) + } + @Composable fun HistoryPreference() { val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) } FeatureSection(GroupFeature.History, enableHistory, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(history = GroupPreference(enable = enable))) } + } + ColumnWithScrollBar { + val titleId = if (groupInfo.useRelays) MR.strings.channel_preferences + else if (groupInfo.businessChat == null) MR.strings.group_preferences + else MR.strings.chat_preferences + AppBarTitle(stringResource(titleId)) + if (!groupInfo.useRelays) { + if (groupInfo.businessChat == null) { + MemberAdmissionButton(openMemberAdmission) + SectionDividerSpaced(maxBottomPadding = false) + } + TimedMessagesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + DirectMessagesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + FullDeletePreference() + SectionDividerSpaced(true, maxBottomPadding = false) + ReactionsPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + VoicePreference() + SectionDividerSpaced(true, maxBottomPadding = false) + FilesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + SimplexLinksPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + ReportsPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + HistoryPreference() + } else { + TimedMessagesPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + FullDeletePreference() + SectionDividerSpaced(true, maxBottomPadding = false) + ReactionsPreference() + SectionDividerSpaced(true, maxBottomPadding = false) + HistoryPreference() + } if (groupInfo.isOwner) { SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + val saveTextId = if (groupInfo.useRelays) MR.strings.save_and_notify_channel_subscribers + else MR.strings.save_and_notify_group_members ResetSaveButtons( reset = reset, save = savePrefs, - disabled = preferences == currentPreferences + disabled = preferences == currentPreferences, + saveTextId = saveTextId ) } SectionBottomSpacer() @@ -253,21 +296,21 @@ private fun FeatureSection( } @Composable -private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { +private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean, saveTextId: StringResource) { SectionView { SectionItemView(reset, disabled = disabled) { Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } SectionItemView(save, disabled = disabled) { - Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + Text(stringResource(saveTextId), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) } } } -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit, confirmTextId: StringResource) { AlertManager.shared.showAlertDialogStacked( title = generalGetString(MR.strings.save_preferences_question), - confirmText = generalGetString(MR.strings.save_and_notify_group_members), + confirmText = generalGetString(confirmTextId), dismissText = generalGetString(MR.strings.exit_without_saving), onConfirm = save, onDismiss = revert, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index d144065399..6e91ad92d6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -119,7 +119,7 @@ fun GroupProfileLayout( ) { Box(contentAlignment = Alignment.TopEnd) { Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, profileImage.value, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f)) + ProfileImage(108.dp, profileImage.value, icon = groupInfo.chatIconName, color = MaterialTheme.colors.secondary.copy(alpha = 0.1f)) EditImageButton { scope.launch { bottomSheetModalState.show() } } } if (profileImage.value != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatLinkHeader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatLinkHeader.kt new file mode 100644 index 0000000000..3c3e4baf49 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatLinkHeader.kt @@ -0,0 +1,79 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.ProfileImage +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun CIChatLinkHeader( + chatLink: MsgChatLink, + ownerSig: LinkOwnerSig?, + hasText: Boolean, +) { + Column( + Modifier + .defaultMinSize(minWidth = 220.dp) + .padding(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 4.dp) + ) { + Row( + Modifier.defaultMinSize(minWidth = 220.dp) + ) { + ProfileImage( + size = 54.dp, + image = chatLink.image, + icon = chatLink.iconRes, + color = if (isInDarkTheme()) FileDark else FileLight + ) + Spacer(Modifier.width(8.dp)) + Column( + Modifier.defaultMinSize(minHeight = 54.dp), + verticalArrangement = Arrangement.Center + ) { + Text( + chatLink.displayName, + style = MaterialTheme.typography.caption, + fontWeight = FontWeight.Medium, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + val fn = chatLink.fullName + if (fn.isNotEmpty() && fn != chatLink.displayName) { + Text(fn, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + Divider(Modifier.fillMaxWidth().padding(top = 8.dp)) + Column(Modifier.padding(top = 8.dp, bottom = 4.dp, start = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) { + chatLink.shortDescription?.let { descr -> + Text( + descr, + color = MaterialTheme.colors.secondary, + fontSize = 13.sp, + lineHeight = 18.sp, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Text( + chatLink.infoLine(signed = ownerSig != null), + color = MaterialTheme.colors.secondary, + fontSize = 13.sp, + lineHeight = 18.sp, + ) + Text( + stringResource(MR.strings.tap_to_open), + color = MaterialTheme.colors.primary, + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 542623028a..afd55ed928 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -127,7 +127,8 @@ fun CIFileView( fun fileIndicator() { Box( Modifier - .size(42.sp.toDp() * sizeMultiplier) + .padding(top = 2.sp.toDp()) + .size(40.sp.toDp() * sizeMultiplier) .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), contentAlignment = Alignment.Center ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index 39bb9545e1..9b8393f66a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* @@ -52,15 +53,12 @@ fun CIGroupInvitationView( else if (isInDarkTheme()) FileDark else FileLight Row( - Modifier - .defaultMinSize(minWidth = 220.dp) - .padding(vertical = 4.dp) - .padding(end = 2.dp) + Modifier.defaultMinSize(minWidth = 220.dp) ) { - ProfileImage(size = 60.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor) - Spacer(Modifier.padding(horizontal = 3.dp)) + ProfileImage(size = 54.dp, image = groupInvitation.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled, color = iconColor) + Spacer(Modifier.width(8.dp)) Column( - Modifier.defaultMinSize(minHeight = 60.dp), + Modifier.defaultMinSize(minHeight = 54.dp), verticalArrangement = Arrangement.Center ) { Text(p.displayName, style = MaterialTheme.typography.caption, fontWeight = FontWeight.Medium, maxLines = 2, overflow = TextOverflow.Ellipsis) @@ -98,8 +96,7 @@ fun CIGroupInvitationView( Box( Modifier .width(IntrinsicSize.Min) - .padding(vertical = 3.dp) - .padding(start = 8.dp, end = 12.dp), + .padding(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 4.dp), contentAlignment = Alignment.BottomEnd ) { Box( @@ -112,10 +109,10 @@ fun CIGroupInvitationView( ) { groupInfoView() val secondaryColor = MaterialTheme.colors.secondary - Column(Modifier.padding(top = 2.dp, start = 5.dp)) { - Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) + Divider(Modifier.fillMaxWidth().padding(top = 8.dp)) + Column(Modifier.padding(top = 8.dp, bottom = 4.dp, start = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) { if (action) { - Text(groupInvitationStr()) + Text(groupInvitationStr(), fontSize = 13.sp, lineHeight = 18.sp) Text( buildAnnotatedString { append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join)) @@ -131,7 +128,9 @@ fun CIGroupInvitationView( buildAnnotatedString { append(groupInvitationStr()) withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) } - } + }, + fontSize = 13.sp, + lineHeight = 18.sp, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index d5e2110063..14afccee54 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -402,7 +402,7 @@ fun ChatItemView( if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { MsgReactionsMenu() } - if (cItem.meta.itemDeleted == null && !live && !cItem.localNote) { + if (cItem.meta.itemDeleted == null && !live && !cItem.localNote && cInfo.sendMsgEnabled) { ItemAction(stringResource(MR.strings.reply_verb), painterResource(MR.images.ic_reply), onClick = { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 8aab0bbbb6..f2788715fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -23,6 +23,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.planAndConnect import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -55,7 +56,7 @@ fun FramedItemView( } @Composable - fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) { + fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean, stripLink: String? = null, prefix: AnnotatedString? = null) { MarkdownText( qi.text, qi.formattedText, @@ -66,11 +67,13 @@ fun FramedItemView( linkMode = linkMode, uriHandler = if (appPlatform.isDesktop) uriHandler else null, showTimestamp = showTimestamp, + prefix = prefix, + stripLink = stripLink, ) } @Composable - fun ciQuotedMsgView(qi: CIQuote) { + fun ciQuotedMsgView(qi: CIQuote, stripLink: String? = null, prefix: AnnotatedString? = null) { Box( Modifier // this width limitation prevents crash on calculating constraints that may happen if you post veeeery long message and then quote it. @@ -89,10 +92,10 @@ fun FramedItemView( style = TextStyle(fontSize = 13.5.sp, color = if (qi.chatDir is CIDirection.GroupSnd) CurrentColors.value.colors.primary else CurrentColors.value.colors.secondary), maxLines = 1 ) - ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp) + ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp, stripLink = stripLink, prefix = prefix) } } else { - ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp) + ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp, stripLink = stripLink, prefix = prefix) } } } @@ -177,6 +180,20 @@ fun FramedItemView( tint = if (isInDarkTheme()) FileDark else FileLight ) } + is MsgContent.MCChat -> { + val prefix = buildAnnotatedString { + append(qi.content.chatLink.displayName + if (qi.content.text != qi.content.chatLink.connLinkStr) " - " else "") + } + Box(Modifier.fillMaxWidth().weight(1f)) { + ciQuotedMsgView(qi, stripLink = qi.content.chatLink.connLinkStr, prefix = prefix) + } + Icon( + painterResource(qi.content.chatLink.smallIconRes), + null, + Modifier.padding(top = 6.dp, end = 4.dp).size(22.dp), + tint = if (isInDarkTheme()) FileDark else FileLight + ) + } else -> ciQuotedMsgView(qi) } } @@ -329,6 +346,22 @@ fun FramedItemView( CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } + is MsgContent.MCChat -> { + val hasText = mc.text != mc.chatLink.connLinkStr + Box( + Modifier.combinedClickable( + onClick = { + withBGApi { planAndConnect(chat.remoteHostId, mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig, close = null) } + }, + onLongClick = { showMenu.value = true } + ) + ) { + CIChatLinkHeader(chatLink = mc.chatLink, ownerSig = mc.ownerSig, hasText = hasText) + } + if (hasText) { + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp, stripLink = mc.chatLink.connLinkStr) + } + } is MsgContent.MCReport -> { val prefix = buildAnnotatedString { withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) { @@ -366,7 +399,8 @@ fun CIMarkdownText( onLinkLongClick: (link: String) -> Unit = {}, showViaProxy: Boolean, showTimestamp: Boolean, - prefix: AnnotatedString? = null + prefix: AnnotatedString? = null, + stripLink: String? = null ) { val chatInfo = chat.chatInfo val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text @@ -382,6 +416,7 @@ fun CIMarkdownText( else -> null }, uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix, + stripLink = stripLink, selectionRange = selection.highlightRange, onTextLayoutResult = selection.onTextLayoutResult ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index b8691c70f8..15d67d8e20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -109,9 +109,12 @@ fun MarkdownText ( showViaProxy: Boolean = false, showTimestamp: Boolean = true, prefix: AnnotatedString? = null, + stripLink: String? = null, selectionRange: IntRange? = null, onTextLayoutResult: ((TextLayoutResult) -> Unit)? = null ) { + val text = if (stripLink != null) stripTextLink(text.toString(), stripLink) else text + val formattedText = if (stripLink != null) stripFormattedTextLink(formattedText, stripLink) else formattedText val textLayoutDirection = remember (text) { if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr } @@ -532,3 +535,20 @@ private fun isRtl(s: CharSequence): Boolean { } fun mentionText(name: String): String = if (name.contains(" @")) "@'$name'" else "@$name" + +fun stripTextLink(text: String, link: String): String = + if (text == link) "" + else if (text.endsWith("\n$link")) text.dropLast(link.length + 1) + else text + +fun stripFormattedTextLink(ft: List?, link: String): List? { + if (ft == null || ft.isEmpty() || ft.last().text != link) return ft + val result = ft.toMutableList() + result.removeAt(result.lastIndex) + val i = result.lastIndex + if (i >= 0 && result[i].format == null && result[i].text.endsWith("\n")) { + result[i] = FormattedText(result[i].text.dropLast(1), null) + if (result[i].text.isEmpty()) result.removeLast() + } + return result.ifEmpty { null } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index a42f66c6cf..8f836c3c29 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -24,7 +24,13 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize import chat.simplex.common.AppLock +import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts @@ -46,7 +52,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds -enum class PresetTagKind { GROUP_REPORTS, FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES } +enum class PresetTagKind { GROUP_REPORTS, FAVORITES, CONTACTS, GROUPS, CHANNELS, BUSINESS, NOTES } sealed class ActiveFilter { data class PresetTag(val tag: PresetTagKind) : ActiveFilter() @@ -234,53 +240,120 @@ private fun ChatListCard( } } +private const val BANNER_IMAGE_RATIO = 800f / 505f + @Composable -private fun AddressCreationCard() { - ChatListCard( - close = { - appPrefs.addressCreationCardShown.set(true) - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.simplex_address), - text = generalGetString(MR.strings.address_creation_instruction), +private fun BannerGradientBox(isDark: Boolean, content: @Composable () -> Unit) { + val stops = if (isDark) darkStops else lightStops + val scale = if (isDark) 1.5f else 1.2f + val gp = gradientPoints(1f / BANNER_IMAGE_RATIO, scale) + var size by remember { mutableStateOf(IntSize.Zero) } + val brush = remember(size, isDark) { + if (size.width > 0 && size.height > 0) { + Brush.linearGradient( + colorStops = stops, + start = Offset(gp.startX * size.width, gp.startY * size.height), + end = Offset(gp.endX * size.width, gp.endY * size.height) ) - }, - onCardClick = { - ModalManager.start.showModal { - UserAddressLearnMore(showCreateAddressButton = true) - } + } else { + Brush.linearGradient(colorStops = stops) } - ) { - Box(modifier = Modifier.matchParentSize().padding(end = (DEFAULT_PADDING_HALF + 2.dp) * fontSizeSqrtMultiplier, bottom = 2.dp), contentAlignment = Alignment.BottomEnd) { - TextButton( - onClick = { - ModalManager.start.showModalCloseable { close -> - UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) - } - }, - ) { - Text(stringResource(MR.strings.create_address_button), style = MaterialTheme.typography.body1) - } + } + Box( + Modifier.fillMaxWidth().aspectRatio(BANNER_IMAGE_RATIO).background(brush).onSizeChanged { size = it }, + contentAlignment = Alignment.Center + ) { content() } +} + +@Composable +private fun ConnectBannerCard() { + val isDark = isInDarkTheme() + val labelBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + .copy(alpha = appPrefs.inAppBarsAlpha.get()) + val buttonSize = 30.dp * fontSizeSqrtMultiplier + val gap = 3.dp * fontSizeSqrtMultiplier + + Column(horizontalAlignment = Alignment.End) { + IconButton( + onClick = { appPrefs.addressCreationCardShown.set(true) }, + modifier = Modifier.size(buttonSize) + ) { + Icon( + painterResource(MR.images.ic_close), + contentDescription = stringResource(MR.strings.icon_descr_close_button), + modifier = Modifier + .size(buttonSize) + .background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f), CircleShape) + .padding(buttonSize * 0.15f), + tint = MaterialTheme.colors.secondary + ) } + Spacer(Modifier.height(gap)) Row( Modifier .fillMaxWidth() - .padding(DEFAULT_PADDING), - verticalAlignment = Alignment.CenterVertically + .height(IntrinsicSize.Min) + .clip(RoundedCornerShape(18.dp)) ) { - Box(Modifier.padding(vertical = 4.dp)) { - Box(Modifier.background(MaterialTheme.colors.primary, CircleShape).padding(12.dp)) { - ProfileImage(size = 37.dp, null, icon = MR.images.ic_mail_filled, color = Color.White, backgroundColor = Color.Red) + Column( + Modifier.weight(1f).clickable { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) + } + } + ) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) MR.images.banner_create_link_light else MR.images.banner_create_link), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + } else { + BannerGradientBox(isDark) { + Icon(painterResource(MR.images.ic_add_link), contentDescription = null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colors.primary) + } + } + Box(Modifier.fillMaxWidth().background(labelBg).padding(vertical = 8.dp), contentAlignment = Alignment.Center) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon(painterResource(MR.images.ic_add_link), contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colors.primary) + Text(stringResource(MR.strings.new_1_time_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } + } else { + Text(stringResource(MR.strings.new_1_time_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } } } - Column(modifier = Modifier.padding(start = DEFAULT_PADDING)) { - Text(stringResource(MR.strings.your_simplex_contact_address), style = MaterialTheme.typography.h3) - Spacer(Modifier.fillMaxWidth().padding(DEFAULT_PADDING_HALF)) - Row(verticalAlignment = Alignment.CenterVertically) { - Text(stringResource(MR.strings.how_to_use_simplex_chat), Modifier.padding(end = DEFAULT_SPACE_AFTER_ICON), style = MaterialTheme.typography.body1) - Icon( - painterResource(MR.images.ic_info), - null, + Spacer(Modifier.width(2.dp).fillMaxHeight().background(MaterialTheme.colors.background)) + Column( + Modifier.weight(1f).clickable { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = close) + } + } + ) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) MR.images.banner_paste_link_light else MR.images.banner_paste_link), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() ) + } else { + BannerGradientBox(isDark) { + Icon(painterResource(MR.images.ic_qr_code_scanner), contentDescription = null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colors.primary) + } + } + Box(Modifier.fillMaxWidth().background(labelBg).padding(vertical = 8.dp), contentAlignment = Alignment.Center) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Icon(painterResource(MR.images.ic_qr_code_scanner), contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colors.primary) + Text(stringResource(if (appPlatform.isAndroid) MR.strings.scan_paste_link else MR.strings.paste_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } + } else { + Text(stringResource(if (appPlatform.isAndroid) MR.strings.scan_paste_link else MR.strings.paste_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground) + } } } } @@ -289,15 +362,31 @@ private fun AddressCreationCard() { @Composable private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState, listState: LazyListState) { - if (!chatModel.desktopNoUserNoRemote) { - ChatList(searchText = searchText, listState) + if (chatModel.chatRunning.value == null) { + Text(stringResource(MR.strings.loading_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } else if (shouldShowOnboarding()) { + if (appPlatform.isAndroid) AndroidOnboardingCards() + } else { + if (!chatModel.desktopNoUserNoRemote) { + ChatList(searchText = searchText, listState) + } + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + Text(stringResource(MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } } - if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { - Text( - stringResource( - if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats - ), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary - ) +} + +@Composable +private fun AndroidOnboardingCards() { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val topPad = topPaddingToContent(false) + val bottomPad = if (oneHandUI.value) { + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier + } else { + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + } + Box(Modifier.fillMaxSize().padding(top = topPad, bottom = bottomPad)) { + ConnectOnboardingView() } } @@ -454,31 +543,33 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow } }, title = { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { - Text( - stringResource(MR.strings.your_chats), - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.SemiBold, - ) - SubscriptionStatusIndicator( - click = { - ModalManager.start.closeModals() - val summary = serversSummary.value - ModalManager.start.showModalCloseable( - endButtons = { - if (summary != null) { - ShareButton { - val json = Json { - prettyPrint = true + if (!shouldShowOnboarding()) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { + Text( + stringResource(MR.strings.your_chats), + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.SemiBold, + ) + SubscriptionStatusIndicator( + click = { + ModalManager.start.closeModals() + val summary = serversSummary.value + ModalManager.start.showModalCloseable( + endButtons = { + if (summary != null) { + ShareButton { + val json = Json { + prettyPrint = true + } + val text = json.encodeToString(PresentedServersSummary.serializer(), summary) + clipboard.shareText(text) } - val text = json.encodeToString(PresentedServersSummary.serializer(), summary) - clipboard.shareText(text) } } - } - ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } - } - ) + ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } + } + ) + } } }, onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, @@ -860,14 +951,6 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } } - if (!addressCreationCardShown.value) { - LaunchedEffect(chatModel.userAddress.value) { - if (chatModel.userAddress.value != null) { - appPrefs.addressCreationCardShown.set(true) - } - } - } - LaunchedEffect(activeFilter.value) { searchText.value = TextFieldValue("") } @@ -914,8 +997,8 @@ private fun ChatListFeatureCards() { if (!oneHandUICardShown.value && !oneHandUI.value) { ToggleChatListCard() } - if (!addressCreationCardShown.value) { - AddressCreationCard() + if (!addressCreationCardShown.value && hasConversations(chatModel.chats.value)) { + ConnectBannerCard() } if (!oneHandUICardShown.value && oneHandUI.value) { ToggleChatListCard() @@ -1236,7 +1319,11 @@ fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat else -> false } PresetTagKind.GROUPS -> when (chatInfo) { - is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null + is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null && !chatInfo.groupInfo.isChannel + else -> false + } + PresetTagKind.CHANNELS -> when (chatInfo) { + is ChatInfo.Group -> chatInfo.groupInfo.isChannel else -> false } PresetTagKind.BUSINESS -> when (chatInfo) { @@ -1255,6 +1342,7 @@ private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair (if (active) MR.images.ic_star_filled else MR.images.ic_star) to MR.strings.chat_list_favorites PresetTagKind.CONTACTS -> (if (active) MR.images.ic_person_filled else MR.images.ic_person) to MR.strings.chat_list_contacts PresetTagKind.GROUPS -> (if (active) MR.images.ic_group_filled else MR.images.ic_group) to MR.strings.chat_list_groups + PresetTagKind.CHANNELS -> (if (active) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_bigtop_updates) to MR.strings.chat_list_channels PresetTagKind.BUSINESS -> (if (active) MR.images.ic_work_filled else MR.images.ic_work) to MR.strings.chat_list_businesses PresetTagKind.NOTES -> (if (active) MR.images.ic_folder_closed_filled else MR.images.ic_folder_closed) to MR.strings.chat_list_notes } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index f5e0389043..346e9bac95 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -31,6 +31,7 @@ import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* +import chat.simplex.common.views.newchat.planAndConnect import chat.simplex.common.views.chat.item.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -241,12 +242,18 @@ fun ChatPreviewView( Text(previewText.first, color = previewText.second) } else if (ci != null && showChatPreviews) { val (text: CharSequence, inlineTextContent) = when { - ci.meta.itemDeleted == null -> ci.text(chat.chatInfo.isChannel) to null - else -> markedDeletedText(ci, chat.chatInfo) to null + ci.meta.itemDeleted != null -> markedDeletedText(ci, chat.chatInfo) to null + ci.content.msgContent is MsgContent.MCChat -> { + val chatLink = (ci.content.msgContent as MsgContent.MCChat).chatLink + val descr = chatLink.shortDescription?.let { "\n$it" } ?: "" + (chatLink.displayName + descr) to null + } + else -> ci.text(chat.chatInfo.isChannel) to null } - val formattedText = when { - ci.meta.itemDeleted == null -> ci.formattedText - else -> null + val formattedText: List? = when { + ci.meta.itemDeleted != null -> null + ci.content.msgContent is MsgContent.MCChat -> null + else -> ci.formattedText } val prefix = when (val mc = ci.content.msgContent) { is MsgContent.MCReport -> @@ -332,6 +339,19 @@ fun ChatPreviewView( withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } } } + is MsgContent.MCChat -> SmallContentPreview(borderColor = if (mc.chatLink.image != null) MaterialTheme.colors.onSurface.copy(alpha = 0.12f) else Color.Transparent) { + Box( + Modifier.fillMaxSize().clickable { withBGApi { planAndConnect(chat.remoteHostId, mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig, close = null) } }, + contentAlignment = Alignment.Center + ) { + val image = mc.chatLink.image + if (image != null) { + Image(base64ToBitmap(image), null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize()) + } else { + Icon(painterResource(mc.chatLink.iconRes), null, Modifier.size(44.sp.toDp()), tint = if (isInDarkTheme()) FileDark else FileLight) + } + } + } else -> {} } } @@ -500,8 +520,8 @@ fun ChatPreviewView( } @Composable -private fun SmallContentPreview(content: @Composable BoxScope.() -> Unit) { - Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(22)).clip(RoundedCornerShape(22))) { +private fun SmallContentPreview(borderColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f), content: @Composable BoxScope.() -> Unit) { + Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(0.5.dp, borderColor, RoundedCornerShape(22)).clip(RoundedCornerShape(22))) { content() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index aa9847c98a..2be17052ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -51,6 +51,9 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } } } + is SharedContent.ChatLink -> { + hasSimplexLink = true + } null -> {} } if (chatModel.chats.value.isNotEmpty()) { @@ -98,7 +101,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal val navButton: @Composable RowScope.() -> Unit = { when { showSearch -> NavigationButtonBack(hideSearchOnBack) - (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward -> { + (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward && remember { chatModel.sharedContent }.value !is SharedContent.ChatLink -> { val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } @@ -129,6 +132,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal chatModel.sharedContent.value = null if (sharedContent is SharedContent.Forward) { chatModel.chatId.value = sharedContent.fromChatInfo.id + } else if (sharedContent is SharedContent.ChatLink) { + chatModel.chatId.value = sharedContent.groupInfo.id } }) } @@ -144,6 +149,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal is SharedContent.Media -> stringResource(MR.strings.share_image) is SharedContent.File -> stringResource(MR.strings.share_file) is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message) + is SharedContent.ChatLink -> stringResource(MR.strings.share_channel) null -> stringResource(MR.strings.share_message) }, color = MaterialTheme.colors.onBackground, @@ -190,7 +196,7 @@ private fun ShareList( val oneHandUI = remember { appPrefs.oneHandUI.state } val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local } + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled }.sortedByDescending { it.chatInfo is ChatInfo.Local } filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 34d8099951..3d670d1c43 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -273,10 +273,11 @@ class AlertManager { profileFullName: String, profileImage: @Composable () -> Unit, subtitle: String? = null, - confirmText: String = generalGetString(MR.strings.connect_plan_open_chat), - onConfirm: () -> Unit, + information: String? = null, + confirmText: String? = generalGetString(MR.strings.connect_plan_open_chat), + onConfirm: (() -> Unit)? = null, dismissText: String = generalGetString(MR.strings.cancel_verb), - onDismiss: (() -> Unit)?, + onDismiss: (() -> Unit)? = null, ) { showAlert { AlertDialog( @@ -325,7 +326,17 @@ class AlertManager { textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, - maxLines = 1, + maxLines = 3, + modifier = Modifier.fillMaxWidth() + ) + } + if (information != null) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text( + information, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2, + maxLines = 3, modifier = Modifier.fillMaxWidth() ) } @@ -341,16 +352,18 @@ class AlertManager { delay(200) focusRequester.requestFocus() } - TextButton(onClick = { - onConfirm.invoke() - hideAlert() - }, Modifier.focusRequester(focusRequester)) { - Text(confirmText) + if (confirmText != null && onConfirm != null) { + TextButton(onClick = { + onConfirm.invoke() + hideAlert() + }, Modifier.focusRequester(focusRequester)) { + Text(confirmText) + } } TextButton(onClick = { onDismiss?.invoke() hideAlert() - }) { + }, if (confirmText == null) Modifier.focusRequester(focusRequester) else Modifier) { Text(dismissText) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt index 096b6c55ac..c7553b6ed0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt @@ -64,7 +64,7 @@ private fun Modifier.androidBlurredModifier( } } .drawBehind { - drawRect(Color.Black) + drawRect(CurrentColors.value.colors.background) if (onTop) { clipRect { if (backgroundGraphicsLayer.size != IntSize.Zero) { @@ -110,7 +110,7 @@ private fun Modifier.desktopBlurredModifier( clip = blurRadius.value > 0 } .drawBehind { - drawRect(Color.Black) + drawRect(CurrentColors.value.colors.background) if (onTop) { clipRect { if (backgroundGraphicsLayer.size != IntSize.Zero) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt index 30811d5c94..cf3281f776 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt @@ -15,6 +15,7 @@ sealed class SharedContent { data class Media(val text: String, val uris: List): SharedContent() data class File(val text: String, val uri: URI): SharedContent() data class Forward(val chatItems: List, val fromChatInfo: ChatInfo): SharedContent() + data class ChatLink(val groupInfo: GroupInfo): SharedContent() } enum class AnimatedViewState { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt index cf10f5e545..e60fcfc921 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt @@ -65,7 +65,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit withBGApi { openGroupChat(null, gInfo.groupId) ModalManager.end.showModalCloseable(true) { close -> - GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, close = close) + GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close) } } } @@ -130,19 +130,31 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit relayIds = relayIds, groupProfile = profile ) - if (result != null) { - val (gI, gL, gR) = result - withContext(Dispatchers.Main) { - chatModel.chatsContext.updateGroup(rhId = null, gI) - chatModel.creatingChannelId.value = gI.id - groupInfo.value = gI - groupLink.value = gL - groupRelays.value = gR.sortedBy { relayDisplayName(it) } - ChannelRelaysModel.set(gI.groupId, gR) - creationInProgress.value = false + when (result) { + is ChatController.PublicGroupCreationResult.Created -> { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId = null, result.groupInfo) + chatModel.creatingChannelId.value = result.groupInfo.id + groupInfo.value = result.groupInfo + groupLink.value = result.groupLink + groupRelays.value = result.groupRelays.sortedBy { relayDisplayName(it) } + ChannelRelaysModel.set(result.groupInfo.groupId, result.groupRelays) + creationInProgress.value = false + } + } + is ChatController.PublicGroupCreationResult.CreationFailed -> { + withContext(Dispatchers.Main) { + creationInProgress.value = false + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_creating_channel), + text = generalGetString(MR.strings.relay_results) + "\n" + + result.addRelayResults.joinToString("\n") { "${chatRelayDisplayName(it.relay)}: ${it.relayError?.let { e -> ChatController.connErrorText(e) } ?: "ok"}" } + ) + } + } + null -> { + withContext(Dispatchers.Main) { creationInProgress.value = false } } - } else { - withContext(Dispatchers.Main) { creationInProgress.value = false } } } catch (e: Exception) { withContext(Dispatchers.Main) { @@ -168,6 +180,7 @@ private suspend fun chooseRandomRelays(): List { val operatorGroups = mutableListOf>() var customRelays = mutableListOf() for (op in servers) { + if (op.operator?.enabled == false) continue val relays = op.chatRelays.filter { it.enabled && !it.deleted && it.chatRelayId != null } if (relays.isEmpty()) continue if (op.operator != null) { @@ -200,6 +213,7 @@ private suspend fun chooseRandomRelays(): List { private suspend fun checkHasRelays(): Boolean { val servers = try { getUserServers(rh = null) } catch (_: Exception) { null } ?: return false return servers.any { op -> + (op.operator?.enabled ?: true) && op.chatRelays.any { it.enabled && !it.deleted && it.chatRelayId != null } } } @@ -249,7 +263,7 @@ private fun ProfileStepView( ) { Box(contentAlignment = Alignment.TopEnd) { Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, image = profileImage.value) + ProfileImage(108.dp, image = profileImage.value, icon = MR.images.ic_bigtop_updates_circle_filled) EditImageButton { scope.launch { bottomSheetModalState.show() } } } if (profileImage.value != null) { @@ -376,7 +390,7 @@ private fun ProgressStepView( Modifier.fillMaxWidth().padding(bottom = 8.dp), contentAlignment = Alignment.Center ) { - ProfileImage(108.dp, image = gInfo.groupProfile.image) + ProfileImage(108.dp, image = gInfo.groupProfile.image, icon = MR.images.ic_bigtop_updates_circle_filled) } Text( gInfo.groupProfile.displayName, @@ -545,11 +559,16 @@ fun relayDisplayName(relay: GroupRelay): String { return "relay ${relay.groupRelayId}" } +private fun chatRelayDisplayName(relay: UserChatRelay): String { + if (relay.displayName.isNotEmpty()) return relay.displayName + return relay.address +} @Composable -fun RelayStatusIndicator(status: RelayStatus, connFailed: Boolean = false) { - val color = if (connFailed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow - val text = if (connFailed) generalGetString(MR.strings.relay_status_failed) else status.text +fun RelayStatusIndicator(status: RelayStatus, connFailed: Boolean = false, memberStatus: GroupMemberStatus? = null) { + val removed = memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + val color = if (connFailed || removed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow + val text = if (connFailed) generalGetString(MR.strings.relay_status_failed) else if (memberStatus == GroupMemberStatus.MemLeft) generalGetString(MR.strings.relay_conn_status_removed_by_operator) else status.text Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 0494cbb463..fa27672270 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -27,6 +27,8 @@ import chat.simplex.common.views.* import chat.simplex.common.views.chat.group.GroupLinkView import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.usersettings.* +import androidx.compose.ui.layout.ContentScale +import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -99,22 +101,33 @@ fun AddGroupLayout( ) { ModalView(close = close) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) - Box( + AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId), bottomPadding = DEFAULT_PADDING_HALF) + Row( Modifier .fillMaxWidth() - .padding(bottom = 24.dp), - contentAlignment = Alignment.Center + .padding(vertical = DEFAULT_PADDING_HALF), + horizontalArrangement = if (BuildConfigCommon.SIMPLEX_ASSETS) Arrangement.SpaceEvenly else Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Box(contentAlignment = Alignment.TopEnd) { - Box(contentAlignment = Alignment.Center) { - ProfileImage(108.dp, image = profileImage.value) - EditImageButton { scope.launch { bottomSheetModalState.show() } } - } - if (profileImage.value != null) { - DeleteImageButton { profileImage.value = null } + Box(contentAlignment = Alignment.Center) { + Box(contentAlignment = Alignment.TopEnd) { + Box(contentAlignment = Alignment.Center) { + ProfileImage(128.dp, image = profileImage.value, icon = MR.images.ic_supervised_user_circle_filled) + EditImageButton { scope.launch { bottomSheetModalState.show() } } + } + if (profileImage.value != null) { + DeleteImageButton { profileImage.value = null } + } } } + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) MR.images.create_group_light else MR.images.create_group), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.height(140.dp) + ) + } } Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 68c7d5b3f1..b38fbf9f51 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -24,6 +24,7 @@ enum class ConnectionLinkType { suspend fun planAndConnect( rhId: Long?, shortOrFullLink: String, + linkOwnerSig: LinkOwnerSig? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, @@ -44,12 +45,13 @@ suspend fun planAndConnect( inProgress.value = false cleanup?.invoke() } - return planAndConnectTask(rhId, shortOrFullLink, close, cleanup, filterKnownContact, filterKnownGroup, inProgress) + return planAndConnectTask(rhId, shortOrFullLink, linkOwnerSig, close, cleanup, filterKnownContact, filterKnownGroup, inProgress) } private suspend fun planAndConnectTask( rhId: Long?, shortOrFullLink: String, + linkOwnerSig: LinkOwnerSig? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, @@ -66,7 +68,7 @@ private suspend fun planAndConnectTask( cleanup?.invoke() completable.complete(!completable.isActive) } - val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, inProgress = inProgress) + val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, linkOwnerSig, inProgress = inProgress) connectProgressManager.stopConnectProgress() if (!inProgress.value) { return completable } if (result != null) { @@ -85,6 +87,7 @@ private suspend fun planAndConnectTask( rhId, connectionLink, connectionPlan.invitationLinkPlan.contactSLinkData_, + ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification, close, cleanup ) @@ -96,6 +99,7 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, cleanup = cleanup, + ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification, ) } InvitationLinkPlan.OwnLink -> { @@ -146,6 +150,7 @@ private suspend fun planAndConnectTask( rhId, connectionLink, connectionPlan.contactAddressPlan.contactSLinkData_, + ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, close, cleanup ) @@ -157,6 +162,7 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, connectDestructive = false, cleanup, + ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, ) } ContactAddressPlan.OwnLink -> { @@ -215,6 +221,7 @@ private suspend fun planAndConnectTask( connectionLink, connectionPlan.groupLinkPlan.groupSLinkInfo_, connectionPlan.groupLinkPlan.groupSLinkData_, + ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, close, cleanup ) @@ -226,6 +233,7 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.you_will_join_group) + linkText, connectDestructive = false, cleanup = cleanup, + ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, ) } is GroupLinkPlan.OwnLink -> { @@ -281,6 +289,33 @@ private suspend fun planAndConnectTask( cleanup() } } + is GroupLinkPlan.NoRelays -> { + Log.d(TAG, "planAndConnect, .GroupLink, .NoRelays") + val groupSLinkData = connectionPlan.groupLinkPlan.groupSLinkData_ + if (groupSLinkData != null) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = groupSLinkData.groupProfile.displayName, + profileFullName = groupSLinkData.groupProfile.fullName, + profileImage = { + ProfileImage( + size = alertProfileImageSize, + image = groupSLinkData.groupProfile.image, + icon = MR.images.ic_bigtop_updates_circle_filled + ) + }, + subtitle = generalGetString(MR.strings.channel_no_active_relays_try_later), + confirmText = null, + dismissText = generalGetString(MR.strings.ok), + onDismiss = { cleanup() } + ) + } else { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.channel_temporarily_unavailable), + generalGetString(MR.strings.channel_no_active_relays_try_later) + ) + cleanup() + } + } } is ConnectionPlan.Error -> { Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}") @@ -348,10 +383,12 @@ fun askCurrentOrIncognitoProfileAlert( text: String? = null, connectDestructive: Boolean, cleanup: (() -> Unit)?, + ownerVerification: OwnerVerification? = null, ) { + val fullText = listOfNotNull(text, ownerVerificationMessage(ownerVerification)).joinToString("\n\n").ifEmpty { null } AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = title, - text = text, + text = fullText, buttons = { Column { val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary @@ -546,6 +583,7 @@ fun showPrepareContactAlert( rhId: Long?, connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, + ownerVerification: OwnerVerification? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -562,6 +600,7 @@ fun showPrepareContactAlert( else MR.images.ic_account_circle_filled ) }, + information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(MR.strings.connect_plan_open_new_chat), onConfirm = { AlertManager.privacySensitive.hideAlert() @@ -587,6 +626,7 @@ fun showPrepareGroupAlert( connectionLink: CreatedConnLink, groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, + ownerVerification: OwnerVerification? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -599,10 +639,11 @@ fun showPrepareGroupAlert( ProfileImage( size = alertProfileImageSize, image = groupShortLinkData.groupProfile.image, - icon = if (isChannel) MR.images.ic_bigtop_updates_padded else MR.images.ic_supervised_user_circle_filled + icon = if (isChannel) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_supervised_user_circle_filled ) }, subtitle = subscriberCount, + information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_new_group), onConfirm = { AlertManager.privacySensitive.hideAlert() @@ -630,3 +671,9 @@ fun showPrepareGroupAlert( } ) } + +fun ownerVerificationMessage(ov: OwnerVerification?): String? = when (ov) { + is OwnerVerification.Verified -> generalGetString(MR.strings.owner_verification_passed) + is OwnerVerification.Failed -> String.format(generalGetString(MR.strings.owner_verification_failed), ov.reason) + null -> null +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 292aa10f70..1eceaf4158 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -74,7 +74,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { Column(Modifier.align(Alignment.BottomCenter)) { DefaultAppBar( navigationButton = { NavigationButtonBack(onButtonClicked = close) }, - fixedTitleText = generalGetString(MR.strings.new_message), + fixedTitleText = generalGetString(MR.strings.new_chat), onTop = false, ) } @@ -359,7 +359,7 @@ private fun ModalData.NewChatSheetLayout( item { Box(Modifier.padding(top = blankSpaceSize)) { AppBarTitle( - stringResource(MR.strings.new_message), + stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index f520a86999..72311cd7fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -21,9 +21,11 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @@ -39,6 +41,7 @@ import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* +import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR import kotlinx.coroutines.* @@ -47,7 +50,7 @@ enum class NewChatOption { } @Composable -fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) { +fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, onboarding: Boolean = false, close: () -> Unit) { val selection = remember { stateGetOrPut("selection") { selection } } val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } } val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } @@ -104,60 +107,71 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } } - BoxWithConstraints { + if (onboarding) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING) - val scope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = selection.value.ordinal, - initialPageOffsetFraction = 0f - ) { NewChatOption.values().size } - KeyChangeEffect(pagerState.currentPage) { - selection.value = NewChatOption.values()[pagerState.currentPage] + Spacer(Modifier.height(DEFAULT_PADDING)) + when (selection.value) { + NewChatOption.INVITE -> PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq, onboarding = true) + NewChatOption.CONNECT -> ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close, onboarding = true) } - TabRow( - selectedTabIndex = pagerState.currentPage, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - tabTitles.forEachIndexed { index, it -> - LeadingIconTab( - selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(it, fontSize = 13.sp) }, - icon = { - Icon( - if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), - it - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) + SectionBottomSpacer() + } + } else { + BoxWithConstraints { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING) + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = selection.value.ordinal, + initialPageOffsetFraction = 0f + ) { NewChatOption.values().size } + KeyChangeEffect(pagerState.currentPage) { + selection.value = NewChatOption.values()[pagerState.currentPage] } - } - - HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> - Column( - Modifier - .fillMaxWidth() - .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), - verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connLinkInvitation.connFullLink.isEmpty()) Arrangement.Center else Arrangement.Top + TabRow( + selectedTabIndex = pagerState.currentPage, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, ) { - Spacer(Modifier.height(DEFAULT_PADDING)) - when (index) { - NewChatOption.INVITE.ordinal -> { - PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq) - } - NewChatOption.CONNECT.ordinal -> { - ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) - } + tabTitles.forEachIndexed { index, it -> + LeadingIconTab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(it, fontSize = 13.sp) }, + icon = { + Icon( + if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), + it + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + + HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> + Column( + Modifier + .fillMaxWidth() + .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), + verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connLinkInvitation.connFullLink.isEmpty()) Arrangement.Center else Arrangement.Top + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + when (index) { + NewChatOption.INVITE.ordinal -> { + PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq) + } + NewChatOption.CONNECT.ordinal -> { + ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) + } + } + SectionBottomSpacer() } - SectionBottomSpacer() } } } @@ -165,12 +179,13 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } @Composable -private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState) { +private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState, onboarding: Boolean = false) { if (connLinkInvitation.connFullLink.isNotEmpty()) { InviteView( rhId, connLinkInvitation = connLinkInvitation, contactConnection = contactConnection, + onboarding = onboarding, ) } else if (creatingConnReq.value) { CreatingLinkProgressView() @@ -448,23 +463,53 @@ fun ActiveProfilePicker( } @Composable -private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState) { +private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState, onboarding: Boolean = false) { val showShortLink = remember { mutableStateOf(true) } - Spacer(Modifier.height(10.dp)) - SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) { + if (onboarding) MR.images.one_time_link_light else MR.images.one_time_link_small_light + } else { + if (onboarding) MR.images.one_time_link else MR.images.one_time_link_small + }), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth() + ) + } else { + Spacer(Modifier.height(10.dp)) + } + + if (onboarding) { + Text( + stringResource(MR.strings.onboarding_send_1_time_link), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true) - } - - Spacer(Modifier.height(DEFAULT_PADDING)) - - SectionViewWithButton( - stringResource(MR.strings.or_show_this_qr_code).uppercase(), - titleButton = if (connLinkInvitation.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null - ) { + Text( + stringResource(MR.strings.onboarding_or_show_qr_code), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() }) + } else { + SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { + LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true) + } + + Spacer(Modifier.height(DEFAULT_PADDING)) + + SectionViewWithButton( + stringResource(MR.strings.or_show_this_qr_code).uppercase(), + titleButton = if (connLinkInvitation.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null + ) { + SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() }) + } } + if (!onboarding) { Spacer(Modifier.height(DEFAULT_PADDING)) val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) { derivedStateOf { @@ -531,6 +576,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared)) } } + } } @Composable @@ -577,13 +623,26 @@ fun AddContactLearnMoreButton() { } @Composable -private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, pastedLink: MutableState, close: () -> Unit) { +private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, pastedLink: MutableState, close: () -> Unit, onboarding: Boolean = false) { DisposableEffect(Unit) { onDispose { connectProgressManager.cancelConnectProgress() } } + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isInDarkTheme()) { + if (onboarding) MR.images.connect_via_link_light else MR.images.connect_via_link_small_light + } else { + if (onboarding) MR.images.connect_via_link else MR.images.connect_via_link_small + }), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth() + ) + } + SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { PasteLinkView(rhId, pastedLink, showQRCodeScanner, close) } @@ -625,7 +684,7 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC } }) { Box(Modifier.weight(1f)) { - Text(stringResource(MR.strings.tap_to_paste_link)) + Text(stringResource(MR.strings.tap_to_paste_link), color = MaterialTheme.colors.primary) } if (connectProgressManager.showConnectProgress != null) { CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f) @@ -681,6 +740,13 @@ fun LinkTextView(link: String, share: Boolean) { // So using BasicTextField + manual ... Text("…", fontSize = 16.sp) if (share) { + Spacer(Modifier.width(DEFAULT_PADDING)) + IconButton({ + chatModel.markShowingInvitationUsed() + clipboard.setText(AnnotatedString(link)) + }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_content_copy), null, tint = MaterialTheme.colors.primary) + } Spacer(Modifier.width(DEFAULT_PADDING)) IconButton({ chatModel.markShowingInvitationUsed() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt new file mode 100644 index 0000000000..98954eb74f --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt @@ -0,0 +1,422 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.UserAddressView +import chat.simplex.res.MR +import kotlinx.coroutines.launch +import kotlin.math.cos +import kotlin.math.sin + +private const val CARD_HEIGHT_RATIO = 0.75f +private const val GRADIENT_ANGLE_RAD = 80.0 * Math.PI / 180.0 + +@Composable +fun shouldShowOnboarding(): Boolean { + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val chats = chatModel.chats.value + return !addressCreationCardShown.value && chats.isNotEmpty() && !hasConversations(chats) +} + +fun hasConversations(chats: List): Boolean = + chats.any { chat -> + when (val c = chat.chatInfo) { + is ChatInfo.Local -> false + is ChatInfo.Direct -> !c.contact.chatDeleted && !c.contact.isContactCard + is ChatInfo.Group -> true + is ChatInfo.ContactRequest -> false + is ChatInfo.ContactConnection -> false + is ChatInfo.InvalidJSON -> false + } + } + +internal data class GradientEndpoints(val startX: Float, val startY: Float, val endX: Float, val endY: Float) + +internal fun gradientPoints(aspectRatio: Float, scale: Float): GradientEndpoints { + val r = aspectRatio.toDouble() + val s = scale.toDouble() + val dx = cos(GRADIENT_ANGLE_RAD) + val dy = -sin(GRADIENT_ANGLE_RAD) / r + val dLenSq = dx * dx + dy * dy + val projections = doubleArrayOf( + -0.5 * dx + (-0.5) * dy, + 0.5 * dx + (-0.5) * dy, + -0.5 * dx + 0.5 * dy, + 0.5 * dx + 0.5 * dy + ) + val tMin = projections.min() + val tMax = projections.max() + val startX = 0.5 + tMin * dx / dLenSq + val startY = 0.5 + tMin * dy / dLenSq + val endX = 0.5 + tMax * dx / dLenSq + val endY = 0.5 + tMax * dy / dLenSq + return GradientEndpoints( + startX = (0.5 + (startX - 0.5) * s).toFloat(), + startY = (0.5 + (startY - 0.5) * s).toFloat(), + endX = (0.5 + (endX - 0.5) * s).toFloat(), + endY = (0.5 + (endY - 0.5) * s).toFloat() + ) +} + +internal val lightStops = arrayOf( + 0.0f to Color(0xFFd2e8ff), + 0.5f to Color(0xFFcce9ff), + 0.9f to Color(0xFFdfffff), + 1.0f to Color(0xFFfffcea) +) + +internal val darkStops = arrayOf( + 0.4f to Color(0xFF040a24), + 0.72f to Color(0xFF3854ab), + 0.9f to Color(0xFFa8edf3), + 1.0f to Color(0xFFfff6e0) +) + +private fun Modifier.maxHeightByWidthRatio(ratio: Float) = layout { measurable, constraints -> + val maxH = (constraints.maxWidth * ratio).toInt().coerceAtMost(constraints.maxHeight) + val placeable = measurable.measure(constraints.copy(minHeight = 0, maxHeight = maxH)) + layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } +} + +@Composable +fun OnboardingCardView( + imageName: dev.icerock.moko.resources.ImageResource, + imageNameLight: dev.icerock.moko.resources.ImageResource, + icon: dev.icerock.moko.resources.ImageResource, + title: String, + subtitle: String? = null, + labelHeightRatio: Float, + onClick: () -> Unit +) { + var imageAreaSize by remember { mutableStateOf(IntSize.Zero) } + val isDark = isInDarkTheme() + val stops = if (isDark) darkStops else lightStops + val scale = if (isDark) 1.5f else 1.2f + + val brush = remember(imageAreaSize, isDark) { + if (imageAreaSize.width > 0 && imageAreaSize.height > 0) { + val aspect = imageAreaSize.height.toFloat() / imageAreaSize.width.toFloat() + val gp = gradientPoints(aspect, scale) + Brush.linearGradient( + colorStops = stops, + start = Offset(gp.startX * imageAreaSize.width, gp.startY * imageAreaSize.height), + end = Offset(gp.endX * imageAreaSize.width, gp.endY * imageAreaSize.height) + ) + } else { + Brush.linearGradient(colorStops = stops) + } + } + + val labelBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + .copy(alpha = appPrefs.inAppBarsAlpha.get()) + + Box( + Modifier + .fillMaxSize() + .clip(RoundedCornerShape(24.dp)) + .clickable(onClick = onClick) + ) { + Column(Modifier.fillMaxSize()) { + Box( + Modifier + .fillMaxWidth() + .weight(1f) + .background(brush) + .onSizeChanged { imageAreaSize = it } + ) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) imageNameLight else imageName), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize() + ) + } else { + Icon( + painterResource(icon), + contentDescription = null, + modifier = Modifier.size(64.dp).align(Alignment.Center), + tint = MaterialTheme.colors.primary + ) + } + } + Box( + Modifier + .fillMaxWidth() + .aspectRatio(1f / labelHeightRatio) + .background(labelBg), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Icon( + painterResource(icon), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colors.primary + ) + } + Text( + title, + style = (if (appPlatform.isDesktop) MaterialTheme.typography.h3 else MaterialTheme.typography.h4).copy(fontWeight = FontWeight.Medium), + color = MaterialTheme.colors.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (subtitle != null) { + Text( + subtitle, + style = if (appPlatform.isDesktop) MaterialTheme.typography.body1 else MaterialTheme.typography.body2, + color = MaterialTheme.colors.onBackground.copy(alpha = 0.7f) + ) + } + } + } + } + } +} + +@Composable +private fun PageHeader(title: String, isLandscape: Boolean, onBack: (() -> Unit)? = null) { + val color = if (onBack != null) MaterialTheme.colors.primary else Color.Transparent + val baseStyle = MaterialTheme.typography.h1 + val titleView = @Composable { + var fontScale by remember(title) { mutableStateOf(1f) } + Text( + title, + style = baseStyle.copy(fontSize = baseStyle.fontSize * fontScale), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + onTextLayout = { result -> + if (result.hasVisualOverflow && fontScale > 0.5f) { + fontScale -= 0.05f + } + } + ) + } + if (isLandscape) { + Box(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING)) { + BackButton(Modifier.align(Alignment.CenterStart), color, onBack) + titleView() + } + } else { + Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING)) { + Box(Modifier.align(Alignment.Start)) { + BackButton(color = color, onClick = onBack) + } + titleView() + } + } +} + +@Composable +private fun BackButton(modifier: Modifier = Modifier, color: Color = MaterialTheme.colors.primary, onClick: (() -> Unit)? = null) { + Row( + modifier + .clip(RoundedCornerShape(20.dp)) + .clickable(enabled = onClick != null, onClick = onClick ?: {}) + .padding(end = 12.dp, top = 10.dp, bottom = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painterResource(MR.images.ic_arrow_back_ios_new), + contentDescription = stringResource(MR.strings.back), + tint = color, + modifier = Modifier.height(24.dp) + ) + Text(stringResource(MR.strings.back), color = color) + } +} + +@Composable +private fun CardPair( + isLandscape: Boolean, + heightRatio: Float, + card1: @Composable () -> Unit, + card2: @Composable () -> Unit +) { + if (isLandscape) { + Row( + Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.weight(1f).maxHeightByWidthRatio(heightRatio)) { card1() } + Box(Modifier.weight(1f).maxHeightByWidthRatio(heightRatio)) { card2() } + } + } else { + Column( + Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), + verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING, Alignment.CenterVertically) + ) { + Box(Modifier.fillMaxWidth().weight(1f, fill = false).maxHeightByWidthRatio(heightRatio)) { card1() } + Box(Modifier.fillMaxWidth().weight(1f, fill = false).maxHeightByWidthRatio(heightRatio)) { card2() } + } + } +} + +@Composable +private fun OnboardingPageLayout( + title: String, + onBack: (() -> Unit)? = null, + cards: @Composable (isLandscape: Boolean) -> Unit +) { + val isLandscape = appPlatform.isDesktop || windowOrientation() == WindowOrientation.LANDSCAPE + Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { + PageHeader(title = title, isLandscape = isLandscape, onBack = onBack) + Box(Modifier.weight(1f).fillMaxWidth().padding(vertical = DEFAULT_PADDING)) { + cards(isLandscape) + } + } +} + +@Composable +fun ConnectOnboardingView() { + val pagerState = rememberPagerState(initialPage = 0) { 2 } + val scope = rememberCoroutineScope() + + val startModalsOpen = appPlatform.isDesktop && ModalManager.start.hasModalsOpen + val cardAlpha by animateFloatAsState(if (startModalsOpen) 0.3f else 1f) + + val cardClickOverride: (() -> Unit)? = if (startModalsOpen) { + { ModalManager.start.closeModals() } + } else null + + fun goToPage(target: Int) { + if (appPlatform.isDesktop) { + scope.launch { pagerState.scrollToPage(target) } + } else { + scope.launch { pagerState.animateScrollToPage(target, animationSpec = tween(350)) } + } + } + + val pager = @Composable { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + userScrollEnabled = !appPlatform.isDesktop + ) { page -> + when (page) { + 0 -> OnboardingPageLayout(title = stringResource(MR.strings.talk_to_someone)) { isLandscape -> + CardPair(isLandscape, CARD_HEIGHT_RATIO, + card1 = { + OnboardingCardView( + imageName = MR.images.card_let_someone_connect_to_you_alpha, + imageNameLight = MR.images.card_let_someone_connect_to_you_alpha_light, + icon = MR.images.ic_add_link, + title = stringResource(MR.strings.let_someone_connect_to_you), + labelHeightRatio = 0.132f, + onClick = cardClickOverride ?: { goToPage(1) } + ) + }, + card2 = { + OnboardingCardView( + imageName = MR.images.card_connect_via_link_alpha, + imageNameLight = MR.images.card_connect_via_link_alpha_light, + icon = MR.images.ic_qr_code_scanner, + title = stringResource(MR.strings.connect_via_link_or_qr_code), + labelHeightRatio = 0.132f, + onClick = cardClickOverride ?: { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, onboarding = true, close = close) + } + } + ) + } + ) + } + 1 -> OnboardingPageLayout( + title = stringResource(MR.strings.connect_with_someone), + onBack = cardClickOverride ?: { goToPage(0) } + ) { isLandscape -> + CardPair(isLandscape, CARD_HEIGHT_RATIO, + card1 = { + OnboardingCardView( + imageName = MR.images.card_invite_someone_privately_alpha, + imageNameLight = MR.images.card_invite_someone_privately_alpha_light, + icon = MR.images.ic_add_link, + title = stringResource(MR.strings.invite_someone_privately), + subtitle = stringResource(MR.strings.a_link_for_one_person), + labelHeightRatio = 0.195f, + onClick = cardClickOverride ?: { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, onboarding = true, close = close) + } + } + ) + }, + card2 = { + OnboardingCardView( + imageName = MR.images.card_create_your_public_address_alpha, + imageNameLight = MR.images.card_create_your_public_address_alpha_light, + icon = MR.images.ic_qr_code, + title = stringResource(if (chatModel.userAddress.value != null) MR.strings.your_public_address else MR.strings.create_your_public_address), + subtitle = stringResource(MR.strings.for_anyone_to_reach_you), + labelHeightRatio = 0.195f, + onClick = cardClickOverride ?: { + ModalManager.start.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, onboarding = true, close = close) + } + } + ) + } + ) + } + } + } + } + + if (appPlatform.isDesktop) { + val maxContentWidth = DEFAULT_WINDOW_WIDTH - DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + Box( + Modifier.fillMaxSize().background(MaterialTheme.colors.background).padding(vertical = DEFAULT_PADDING).graphicsLayer { alpha = cardAlpha }, + contentAlignment = Alignment.Center + ) { + Box(Modifier.widthIn(max = maxContentWidth).fillMaxHeight()) { + pager() + } + } + } else { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + pager() + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index f64f1dcecd..89b2f97ee7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -880,6 +880,38 @@ private val versionDescriptions: List = listOf( ), ) ), + VersionDescription( + version = "v6.5", + post = "https://simplex.chat/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html", + features = listOf( + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v6_5_public_channels, + descrId = null, + subfeatures = listOf( + MR.images.ic_wifi_tethering to MR.strings.v6_5_reliability, + MR.images.ic_dns to MR.strings.v6_5_ownership, + MR.images.ic_vpn_key_filled to MR.strings.v6_5_security, + MR.images.ic_shield to MR.strings.v6_5_privacy, + ) + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_add_link, + titleId = MR.strings.v6_5_invite_friends, + descrId = MR.strings.v6_5_invite_friends_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_security, + titleId = MR.strings.v6_5_safe_web_links, + descrId = MR.strings.v6_5_safe_web_links_descr + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_verified_user, + titleId = MR.strings.v6_5_non_profit_governance, + descrId = MR.strings.v6_5_non_profit_governance_descr + ), + ) + ), ) private val lastVersion = versionDescriptions.last().version diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 3b6cf34b7c..e5c731f3b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -7,7 +7,9 @@ import SectionTextFooter import SectionView import SectionViewWithButton import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.ui.layout.ContentScale import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* @@ -28,6 +30,7 @@ import chat.simplex.common.model.MsgContent import chat.simplex.common.platform.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.newchat.* +import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR @Composable @@ -35,6 +38,7 @@ fun UserAddressView( chatModel: ChatModel, shareViaProfile: Boolean = false, autoCreateAddress: Boolean = false, + onboarding: Boolean = false, close: () -> Unit ) { // TODO close when remote host changes @@ -75,17 +79,31 @@ fun UserAddressView( addressSettings = AddressSettings(businessAddress = false, autoAccept = null, autoReply = null) ) - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.share_address_with_contacts_question), - text = generalGetString(MR.strings.add_address_to_your_profile), - confirmText = generalGetString(MR.strings.share_verb), - onConfirm = { - setProfileAddress(true) - shareViaProfile.value = true - } - ) + val hasRelevantContacts = chatModel.chats.value.any { chat -> + val ci = chat.chatInfo + ci is ChatInfo.Direct && + ci.contact.active && + !ci.contact.isContactCard && + !ci.contact.contactConnIncognito + } + if (hasRelevantContacts) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.share_address_with_contacts_question), + text = generalGetString(MR.strings.add_address_to_your_profile), + confirmText = generalGetString(MR.strings.share_verb), + onConfirm = { + setProfileAddress(true) + shareViaProfile.value = true + } + ) + progressIndicator.value = false + } else { + setProfileAddress(true) + shareViaProfile.value = true + } + } else { + progressIndicator.value = false } - progressIndicator.value = false } } @@ -103,6 +121,7 @@ fun UserAddressView( user = user.value, userAddress = userAddress.value, shareViaProfile, + onboarding = onboarding, createAddress = ::createAddress, showAddShortLinkAlert = { shareAddress: (() -> Unit)? -> showAddShortLinkAlert(progressIndicator = progressIndicator, share = ::share, shareAddress = shareAddress) @@ -249,6 +268,7 @@ private fun UserAddressLayout( user: User?, userAddress: UserContactLinkRec?, shareViaProfile: MutableState, + onboarding: Boolean = false, createAddress: () -> Unit, showAddShortLinkAlert: ((() -> Unit)?) -> Unit, learnMore: () -> Unit, @@ -259,68 +279,100 @@ private fun UserAddressLayout( saveAddressSettings: (AddressSettingsState, MutableState) -> Unit, ) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) + if (!onboarding) { + AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) + } + if (BuildConfigCommon.SIMPLEX_ASSETS && userAddress != null) { + Image( + painterResource(if (isInDarkTheme()) { + if (onboarding) MR.images.simplex_address_light else MR.images.simplex_address_small_light + } else { + if (onboarding) MR.images.simplex_address else MR.images.simplex_address_small + }), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth() + ) + } Column( Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { if (userAddress == null) { - SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { - CreateAddressButton(createAddress) - } + if (!onboarding) { + SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { + CreateAddressButton(createAddress) + } - SectionDividerSpaced() - SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { - CreateOneTimeLinkButton() - } + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - SectionView { - LearnMoreButton(learnMore) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionView { + LearnMoreButton(learnMore) + } } } else { - val addressSettingsState = remember { mutableStateOf(AddressSettingsState(settings = userAddress.addressSettings)) } - val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) } val showShortLink = remember { mutableStateOf(true) } - SectionViewWithButton( - stringResource(MR.strings.for_social_media).uppercase(), - titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null - ) { + if (onboarding) { + Text( + stringResource(MR.strings.onboarding_post_address), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) + LinkTextView(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value), true) + Text( + stringResource(MR.strings.onboarding_or_use_qr_code), + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + style = MaterialTheme.typography.body1 + ) SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value) - if (userAddress.shouldBeUpgraded) { - AddShortLinkButton(text = stringResource(MR.strings.add_short_link)) { showAddShortLinkAlert(null) } - } - ShareAddressButton { + } else { + val addressSettingsState = remember { mutableStateOf(AddressSettingsState(settings = userAddress.addressSettings)) } + val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) } + + SectionViewWithButton( + stringResource(MR.strings.for_social_media).uppercase(), + titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null + ) { + SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value) if (userAddress.shouldBeUpgraded) { - showAddShortLinkAlert { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } - } else { - share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) + AddShortLinkButton(text = stringResource(MR.strings.add_short_link)) { showAddShortLinkAlert(null) } + } + ShareAddressButton { + if (userAddress.shouldBeUpgraded) { + showAddShortLinkAlert { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } + } else { + share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) + } + } + // ShareViaEmailButton { sendEmail(userAddress) } + BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } + AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) + + if (addressSettingsState.value.businessAddress) { + SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) } } - // ShareViaEmailButton { sendEmail(userAddress) } - BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } - AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) - if (addressSettingsState.value.businessAddress) { - SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) + SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress) + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + LearnMoreButton(learnMore) } - } - SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress) - SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { - CreateOneTimeLinkButton() - } - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - LearnMoreButton(learnMore) - } - - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - DeleteAddressButton(deleteAddress) - SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + DeleteAddressButton(deleteAddress) + SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 2a76fa292a..3045f3035d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -2541,4 +2541,5 @@ ابحث عن رسائل صوتية فيديوهات رسائل صوتية + إذا انضممت إلى قنوات أو أنشأتها، فستتوقف عن العمل نهائيًا. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 9ea32d5131..a9b2045b97 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -191,6 +191,8 @@ Please check that you used the correct link or ask your contact to send you another one. Unsupported connection link This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Channel temporarily unavailable + Channel has no active relays. Please try to join later. Connection error (AUTH) Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection. Connection blocked @@ -463,6 +465,15 @@ Tap to start a new chat Chat with the developers You have no chats + Talk to someone + Let someone connect to you + Connect via link or QR code + Connect with someone + Invite someone privately + A link for one person to connect + Create your public address + Your public address + For anyone to reach you Loading chats… No filtered chats No chats in list %s. @@ -495,6 +506,7 @@ Favorites Contacts Groups + Channels Businesses Notes Reports @@ -531,8 +543,21 @@ Share file… Forward message… Forward messages… + Share channel… Cannot send message Selected chat preferences prohibit this message. + Share via chat + Tap to open + Channel link + Group link + Business address + Contact address + One-time link + (from owner) + (signed) + Error sharing channel + Link signature verified. + ⚠️ Signature verification failed: %s. Attach @@ -591,6 +616,7 @@ removed from group you left can\'t send messages + can\'t broadcast you are observer reviewed by admins member has old version @@ -887,7 +913,7 @@ New chat New message Add contact - Scan / Paste link + Paste link / Scan Paste link One-time invitation link 1-time link @@ -1115,12 +1141,12 @@ All your contacts will remain connected. All your contacts will remain connected. Profile update will be sent to your contacts. Share link - Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. Create an address to let people connect with you. Create SimpleX address - Share with contacts - Share address with contacts? - Profile update will be sent to your contacts. + Share with SimpleX contacts + Share address with SimpleX contacts? + Profile update will be sent to your SimpleX contacts. Stop sharing address? Stop sharing Auto-accept @@ -1137,6 +1163,11 @@ Or to share privately SimpleX address or 1-time link? Create 1-time link + New 1-time link + Send the link via any messenger - it\'s secure. Ask to paste into SimpleX. + Or show QR in person or via video call. + Use this address in your social media profile, website, or email signature. + Or use this QR - print or show online. Address settings Business address Add your team members to the conversations. @@ -1859,6 +1890,7 @@ Error creating member contact Error sending invitation Only group owners can change group preferences. + Only channel owners can change channel preferences. Only chat owners can change preferences. Address Share address @@ -2201,6 +2233,7 @@ Chat preferences Contact preferences Group preferences + Channel preferences Set group preferences Set member admission Your preferences @@ -2546,6 +2579,17 @@ Share your address 4 new interface languages Catalan, Indonesian, Romanian and Vietnamese - thanks to our users! + Public channels - speak freely 🚀 + Reliability: many relays per channel. + Ownership: you can run your own relays. + Security: owners hold channel keys. + Privacy: for owners and subscribers. + Easier to invite your friends 👋 + We made connecting simpler for new users. + Safe web links + - opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking. + Non-profit governance + To make SimpleX Network last. View updated conditions @@ -2881,17 +2925,30 @@ connecting deleted failed + removed by operator new invited accepted active + inactive + All relays removed + All relays failed + No active relays + %1$d relays removed + %1$d relays failed + %1$d relays not active %1$d/%2$d relays active, %3$d failed + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d relays active, %3$d errors %1$d/%2$d relays active %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d relays connected, %3$d removed %1$d/%2$d relays connected - %1$d relays + Adding relays will be supported later. + Waiting for channel owner to add relays. RELAY @@ -2914,6 +2971,10 @@ Channel name Creating channel Error creating channel + Relay results: + The connection reached the limit of undelivered messages + Network error + Error Cancel creating channel? Cancel Enable at least one chat relay to create a channel. @@ -2937,4 +2998,11 @@ Unblock subscriber for all? + + + Enable link previews? + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + Enable + Disable + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index b559431261..43afc3be04 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -49,7 +49,7 @@ Vytvoření tajné skupiny Zadejte název skupiny: Úplný název skupiny: - Váš chat profil bude zaslán členům skupiny + Váš profil chatu bude zaslán členům skupiny Profil skupiny je uložen v zařízeních členů, nikoli na serverech. Uložit Aktualizovat nastavení sítě\? @@ -228,7 +228,7 @@ Použít proxy server SOCKS\? Použít přímé připojení k internetu\? Ne - Chat profil + Profil chatu Připojení simplexmq: v%s (%2s) Vytvořit adresu @@ -276,9 +276,9 @@ Chyba při exportu databáze chatu Import Restartujte aplikaci, abyste mohli používat importovanou databázi chatu. - Smazat chat profil\? + Smazat profil chatu? Tuto akci nelze vzít zpět! Váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny. - Restartujte aplikaci a vytvořte nový chat profil. + Restartujte aplikaci a vytvořte nový profil chatu. Nejnovější verzi databáze chatu musíte používat POUZE v jednom zařízení, jinak se může stát, že přestanete přijímat zprávy od některých kontaktů. Soubory a média Smazat soubory a média\? @@ -315,7 +315,7 @@ změnila se vaše adresa Rozšířit výběr rolí Nelze pozvat kontakt! - Již máte chat profil se stejným názvem. Zvolte prosím jiné jméno. + Již máte profil chatu se stejným názvem. Zvolte prosím jiné jméno. Vytvořit frontu Zabezpečit frontu Okamžitá oznámení! @@ -372,7 +372,7 @@ Vaše nastavení Vaše SimpleX adresa Přístupová fráze k databázi a export - Vaše chat profily + Profily chatu Zaslat otázky a nápady Test serveru Servery ICE (jeden na řádek) @@ -462,7 +462,7 @@ Chyba při odebrání člena Chyba při ukládání profilu skupiny vteřiny - Umožňuje mít v jednom chat profilu mnoho anonymních spojení bez sdílení údajů mezi nimi. + Umožňuje mít v jednom profilu chatu mnoho anonymních spojení bez sdílení údajů mezi nimi. Pokud s někým sdílíte inkognito profil, bude použit pro skupiny, do kterých vás pozve. Systémové Hlasové zprávy @@ -484,7 +484,7 @@ Ověření zabezpečení připojení Francouzské rozhraní Díky uživatelům - překládejte prostřednictvím Weblate! - Více chat profilů + Více profilů chatu Různá jména, avataři a izolace přenosu. Návrh zprávy Zachování posledního návrhu zprávy s přílohami. @@ -615,7 +615,7 @@ Adresa vašeho serveru Použít server Použít pro nová připojení - Servery pro nová připojení vašeho aktuálního chat profilu. + Servery pro nová připojení v rámci vašeho aktuálního profilu chatu. Použít SimpleX Chat servery\? Použití SimpleX Chat serverů. Uložené servery WebRTC ICE budou odstraněny. @@ -629,7 +629,7 @@ Onion hostitelé budou použiti, pokud jsou k dispozici. Onion hostitelé nebudou použiti. Izolace přenosu - for each chat profile you have in the app.]]> + pro každý profil chatu, který máte v aplikaci.]]> pro všechny kontakty a členy skupin. \nUpozornění: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.]]> Vzhled Verze aplikace @@ -726,7 +726,7 @@ Chyba při mazání databáze chatu Chyba při importu databáze chatu Databáze chatu odstraněna - Odstranit soubory všech chat profilů + Odstranit soubory všech profilů chatu Odstranit všechny soubory Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány. Žádné přijaté ani odeslané soubory @@ -734,7 +734,7 @@ nikdy %s sekund(y) Zprávy - Toto nastavení se vztahuje na zprávy ve vašem aktuálním chat profilu. + Toto nastavení se vztahuje na zprávy ve vašem aktuálním profilu chatu. Smazat zprávy po Povolit automatické mazání zpráv\? Tuto akci nelze vzít zpět - zprávy odeslané a přijaté dříve, než bylo zvoleno, budou smazány. Může to trvat několik minut. @@ -862,7 +862,7 @@ Povolit TCP keep-alive Aktualizovat Smazat profil chatu? - Smazat chat profil pro + Smazat profil chatu pro Profil a připojení k serveru Pouze místní data profilu Režim inkognito chrání vaše soukromí používáním nového náhodného profilu pro každý kontakt. @@ -872,7 +872,7 @@ Kontakt povolil zaplé vyplé - Chat předvolby + Předvolby chatu Předvolby kontaktu Předvolby skupiny Přímé zprávy @@ -923,8 +923,7 @@ Italské rozhraní Díky uživatelům - překládejte prostřednictvím Weblate! Budete připojeni, jakmile bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později! - Váš chat profil bude odeslán -\nvašemu kontaktu + Váš profil chatu bude odeslán \nvašemu kontaktu Konverzace Sdílet jednorázovou pozvánku koncově šifrované @@ -953,7 +952,7 @@ Chyba aktualizace soukromí uživatele Správa skupin Uvítací zpráva skupin - Skryté chat profily + Skryté profily chatu Hesla skrytých profilů Skrýt Skrýt profil @@ -964,7 +963,7 @@ \n- zakázat členy (role "pozorovatel") Uložit heslo profilu Ztlumit - Chraňte své chat profily heslem! + Chraňte své profily chatu pomocí hesla! Uložit a aktualizovat profil skupiny Ztlumit při neaktivitě! Heslo k zobrazení @@ -976,7 +975,7 @@ Uvítací zpráva Uvítací zpráva Zrušit ztlumení - Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce Chat profily. + Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce Profily chatu. Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. Můžete skrýt nebo ztlumit uživatelský profil - Podržte pro menu. Odkrýt @@ -998,10 +997,10 @@ Zobrazit: ID databáze a možnost Izolace přenosu. Soubor bude přijat, jakmile váš kontakt dokončí nahrávání. - Smazat chat profil + Smazat profil chatu Smazat profil Heslo profilu - Odkrýt chat profil + Odkrýt profil chatu Odkrýt profil Žádost o přijetí videa Současně lze odeslat pouze 10 videí @@ -1104,7 +1103,7 @@ Heslo pro sebedestrukci změněno! Další zbarvení Sekundární - Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle. + Vytvořit prázdný profil chatu se zadaným názvem a otevřít aplikaci jako obvykle. Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána! Další sekundární Pozadí @@ -1114,10 +1113,10 @@ Chyba načítání podrobností Info Hledat - Změnit chat profily + Změnit profily chatu Historie Přijatá zpráva - Poslaná zpráva + Odeslaná zpráva Mizící zpráva Poslat mizící zprávu 1 minutu @@ -1248,7 +1247,7 @@ šifrování povoleno pro %s vyžadováno opětovné vyjednávání šifrování pro %s Odesílání potvrzení o doručení je vypnuto pro %d kontakty. - Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných chat profilech. + Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu. Toto nastavení je pro váš aktuální profil opětovné vyjednávání šifrování povoleno opětovné vyjednávání šifrování povoleno pro %s @@ -1406,7 +1405,7 @@ Odpojit počítač? Vytvořit skupinu Kód relace - Vložit adres počítače + Vložit adresu počítače Blokovaný Lepší skupiny Rozbalit @@ -1471,7 +1470,7 @@ Blokovat člena Použít z PC Opakovat požadavek na připojení? - Vytvořit chat profil + Vytvořit profil chatu Zobrazit havarované Smazat a informovat kontakt Již se připojujete přes tento jednorázový odkaz! @@ -1744,7 +1743,7 @@ Kamera Kamera a mikrofon SimpleX odkazy jsou zakázány. - koncovým šifrováním s dokonalým dopředným utajením, odmítnutím a obnovením po vloupání.]]> + koncovým šifrováním s dokonalým dopředným utajením, odmítnutím a obnovením po vloupání.]]> Pokročilé nastavení Všechny barevné režimy Překročená kapacita - příjemci neobdrží dříve poslané zprávy. @@ -1902,7 +1901,7 @@ Chat databáze exportována Členu skupiny nelze odeslat zprávu Požádejte váš kontakt ať povolí volání. - Odeslaných odpovědí + Odeslaná odpověď Škálovat Přizpůsobit Rozmazání pro lepší soukromí. @@ -1946,7 +1945,7 @@ Smazat bez upozornění hledat Chyba přepínání profilu - Vyberte chat profil + Vyberte profil chatu zpráva otevřít Kontakt smazán! @@ -2241,7 +2240,7 @@ Můžete nastavit operátory v nastavení sítě a serverů. Ocas Zastavíte přijímání zpráv z tohoto chatu. Chat historie bude zachována. - Servery pro nové soubory vašeho aktuálního chat profilu + Servery pro nové soubory vašeho aktuálního profilu chatu Protokolu serveru se změnil. Operátor serveru se změnil. Zoom @@ -2266,7 +2265,7 @@ Použit pro zprávy Server přidán k operátoru %s. Průhlednost - Přepínání chat profilu pro 1-rázové pozvánky. + Přepínání profilu chatu pro jednorázové pozvánky. video Sdílet profil Reportování zpráv je zakázáno v této skupině. @@ -2288,7 +2287,7 @@ Pro ochranu vaší IP adresy, soukromé směrování používá vaše servery SMP k doručování zpráv. Použít web portu TCP port pro zprávy - Váš chat profil bude zaslán členům + Váš profil chatu bude zaslán členům chatu Režim systému Zmínky členů 👋 Organizujte konverzace do seznamů @@ -2331,7 +2330,7 @@ Použít TCP port %1$s, když není zadán žádný port. Tento odkaz byl použit s jiným mobilním zařízením, vytvořte na počítači nový odkaz. Získejte upozornění, když jste zmíněni. - SimpleX adresa a 1 rázové odkazy je bezpečné sdílet přes všechny komunikátory. + SimpleX adresa a jednorázové odkazy je bezpečné sdílet přes všechny komunikátory. Zprávy budou smazány pro všechny členy. Aplikace vyžaduje potvrzení stahování z neznámých serverů (s výjimkou .onion nebo při aktivaci SOCKS proxy). Musíte povolit kontaktům volání, abyste jim mohli zavolat. @@ -2550,4 +2549,6 @@ Připojení selhalo selhal Pokud jste se připojili k nějakým kanálům nebo je vytvořili, přestanou trvale fungovat. + aktivní + Zrušit diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 5d3d9ac90e..c66d351601 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -1971,7 +1971,7 @@ Abgelaufen Server-Einstellungen öffnen Andere Fehler - Proxy + Proxy-vermittelt Fehler beim Empfang Neu verbinden Deaktiviert @@ -2605,7 +2605,7 @@ Link-Tracking entfernen Verbinden tippen, um den Bot zu nutzen. Um Befehle senden zu können, müssen Sie verbunden sein. - SimpleX Relais-Link + SimpleX Relais-Adresse Fehler beim Markieren als gelesen Fingerabdruck in der Zielserveradresse stimmt nicht mit dem Zertifikat überein: %1$s. Fingerabdruck in der Weiterleitungsserveradresse stimmt nicht mit dem Zertifikat überein: %1$s. @@ -2629,7 +2629,117 @@ Sprachnachrichten suchen Videos Sprachnachrichten - Verbindung fehlgeschlagen + VERBINDUNG FEHLGESCHLAGEN Fehlgeschlagen Kanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert. + %1$d/%2$d Relais aktiv + %1$d/%2$d Relais aktiv, %3$d Fehlgeschlagen + %1$d/%2$d Relais verbunden + %1$d/%2$d Relais verbunden, %3$d Fehler + %1$d Abonnent + %1$d Abonnenten + Angenommen + Aktiv + Abonnenten für alle blockieren? + Broadcast + Abbrechen + Kanalerstellung abbrechen? + Relais testen, um dessen Namen zu ermitteln.]]> + %1$s!]]> + Kanal + Kanal + Kanal + Kanal-Link + Kanal-Mitglieder + Kanal-Name + Der Kanal wird für alle Abonnenten gelöscht. Dies kann nicht rückgängig gemacht werden! + Der Kanal wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! + Der Kanal wird mit %1$d von %2$d Relais gestartet. Fortfahren? + Chat-Relais + Chat-Relais + Chat-Relais + Chat-Relais + Chat‑Relais leiten Nachrichten in den von Ihnen erstellten Kanälen weiter. + Chat‑Relais leiten Nachrichten an Kanal-Abonnenten weiter. + Relais-Adresse überprüfen und erneut versuchen. + Relais-Name überprüfen und erneut versuchen. + Relais konfigurieren + Verbinden + verbunden + verbinden + Kanal erstellen + Kanal erstellen + Kanal erstellen (BETA) + Kanal wird erstellt + Link dekodieren + Kanal löschen + Kanal löschen? + Gelöscht + Relais löschen + Kanalprofil bearbeiten + Aktiviere mindestens ein Chat‑Relais, um einen Kanal zu erstellen. + Relais-Name eingeben… + Fehler beim Hinzufügen des Relais + Fehler beim Erstellen des Kanals + Fehler beim Öffnen des Kanals + Fehlgeschlagen + Fehlgeschlagen + Link erhalten + Ungültige Relais-Adresse! + Ungültiger Relais-Name! + Eingeladen + Kanal beitreten + Kanal verlassen + Kanal verlassen? + Link + Neu + Neues Chat-Relais + Keine Chat-Relais + Es sind keine Chat-Relais aktiviert. + Es sind nicht alle Relais verbunden + Kanal öffnen + Neuen Kanal öffnen + EIGENTÜMER + Eigentümer + Voreingestellte Relais-Adresse + Voreingestellter Relais-Name + Fortfahren + Relais + RELAIS + Relais-Adresse + Relais-Adresse + Relais-Verbindung fehlgeschlagen + Relais-Link + Relais-Test fehlgeschlagen! + Abonnent entfernen + Abonnent entfernen? + Der Server erfordert eine Autorisierung, um eine Verbindung zum Relais herzustellen. Bitte Passwort überprüfen. + Serverwarnung + Relais-Adresse teilen + ABONNENT + Abonnenten + Abonnenten verbinden sich über den Relais‑Link mit dem Kanal.\nDie Relais-Adresse wurde zur Einrichtung dieses Relais für diesen Kanal verwendet. + Abonnent wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden! + Tippen, um dem Kanal beizutreten + Der Test ist bei Schritt %s fehlgeschlagen. + Relais testen + Dies ist eine Chat‑Relais-Adresse, welche nicht zum Verbinden verwendet werden kann. + Abonnent für alle freigeben? + Für neue Kanäle verwenden + Relais nutzen + Überprüfen + via %1$s + Sprachaufnahmen werden auf Ihrer Plattform nicht unterstützt. + Abwarten + Antwort abwarten + Sie + Sie sind Abonnent + Sie können einen Link oder QR-Code teilen - damit kann jeder dem Kanal beitreten. + Sie haben sich über diesen Relais‑Link mit dem Kanal verbunden. + Ihr Kanal + Ihr Kanal + Ihr Profil %1$s wird mit den Kanal‑Relais und -Abonnenten geteilt. + Ihre Relais-Adresse + Ihr Relais-Name + Sie werden keine Nachrichten mehr aus diesem Kanal erhalten. Der Chatverlauf bleibt erhalten. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 563b1a02c7..42ba84ef8c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -85,7 +85,7 @@ Les notifications périodiques sont désactivées ! Une phrase secrète est nécessaire Autoriser le dans la boîte de dialogue suivante pour recevoir des notifications instantanément.]]> - Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe + Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe. L\'application récupère périodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les données de votre appareil ne sont pas envoyées aux serveurs. SimpleX fonctionne en arrière-plan au lieu d\'utiliser les notifications push.]]> Cacher @@ -1017,7 +1017,7 @@ Supprimer le fichier Erreur lors de la sauvegarde des serveurs XFTP Assurez-vous que les adresses des serveurs XFTP sont au bon format, séparées par des lignes et qu\'elles ne sont pas dupliquées. - Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe + Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe. Téléverser le fichier Serveurs XFTP Vos serveurs XFTP @@ -1056,7 +1056,7 @@ Système Authentification annulée ID du message incorrect - Le hash du message précédent est différent.\" + Le hash du message précédent est différent. L\'ID du message suivant est incorrect (inférieur ou égal au précédent). \nCela peut se produire en raison d\'un bug ou lorsque la connexion est compromise. Erreur de déchiffrement @@ -1149,7 +1149,7 @@ Assurez-vous que le fichier a une syntaxe YAML correcte. Exporter le thème pour avoir un exemple de la structure du fichier du thème. La mise à jour du profil sera envoyée à vos contacts. Guide de l\'utilisateur.]]> - Enregistrer les paramètres de validation automatique + Enregistrer les paramètres de l\'adresse SimpleX Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l\'app. Le code d\'accès de l\'application est remplacé par un code d\'autodestruction. Activer l\'autodestruction @@ -1950,7 +1950,7 @@ Infos serveurs Afficher les informations pour À partir de %s. - À partir de %s. \nToutes les données restent confinées dans votre appareil. + À partir de %s.\nToutes les données restent confinées dans votre appareil. Statistiques Total Serveur XFTP @@ -2361,4 +2361,92 @@ Quatres nouvelles langues d\'interface Partagez votre adresse Actualisez votre adresse + 1 discussion avec un membre + vous a accepté + Accepter le membre + actif + tous + Tous les messages + Permettre des fichiers et des médias seulement si votre contact les permet. + Permettre à vos contacts d\'envoyer des fichiers et des médias. + Tous les serveurs + Annuler + seulement après que votre requête soit acceptée.]]> + Vérifiez l\'adresse de relais et essayez à nouveau. + Vérifiez le nom du relais et essayez à nouveau. + Se connecter + Se connecter + connecté + CONNEXION ÉCHOUÉE + contact supprimé + contact désactivé + le contact devrait accepter… + Créez votre adresse + supprimé + Supprimer les messages + Supprimer le relais + %d messages + Entrez le nom de relais.. + échoué + échoué + Fichiers + Les membres seront retirés du groupe - impossible de revenir en arrière! + Le membre va rejoindre le groupe, accepter le membre? + Les messages de ces membres seront affichés! + modérateurs + nouveau + Nouveau rôle de groupe: Modérateur + non synchronisé + Désactivé + Ouvrir pour se connecter + Ouvrez pour rejoindre + en attente + en attente d\'approbation + Veuillez attendre que les modérateurs de groupe examinent votre demande pour rejoindre le groupe. + Adresse de relais prédéfinie + Nom de relais prédéfini + Serveurs prédéfinis + Politique de confidentialité et conditions d\'utilisation. + Continuer + Rejeter + Rejeter la demande de contact + rejeté + rejeté + Rejeter le membre? + relais + RELAIS + Adresse de relais + Adresse de relais + Échec de la connexion au relais + Lien du relais + Retirer et supprimer les messages + retiré du groupe + Retirer les membres? + Retire les messages et bloque les membres. + Rapport envoyé aux modérateurs + requête envoyée + examiné par les administrateurs + Examiner les membres du groupe + Examiner les membres + Envoyez une demande de contact? + Envoyer la demande + Envoyer la demande sans message + Envoyez vos commentaires privés aux groupes. + Envoyé à votre contact après la connexion. + Le serveur requiert une autorisation pour se connecter au relais, vérifiez le mot de passe. + Avertissement du serveur + Partager l\'ancienne adresse + Partager l\'ancien lien + Partager l\'adresse de relais + Description courte: + Lien court + Adresse SimpleX courte + Adresse du relais SimpleX + Port TCP pour la messagerie + Échec du test à l\'étape %s. + Tester le relais + L\'adresse sera courte et votre profil sera partagé via l\'adresse. + Le lien sera court, et le profil de groupe sera partagé via le lien. + L\'expéditeur n\'en sera PAS informé. + Ce réglage est pour votre profil actuel diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 09a843c91c..310a52715a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -45,7 +45,7 @@ Az Android Keystore-t a jelmondat biztonságos tárolására használják – lehetővé teszi az értesítési szolgáltatás működését. Hibás az üzenet kivonata Háttér - Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének küldése közvetlen kapcsolatot használ.]]> + Megjegyzés: az üzenet- és a fájlátjátszók SOCKS proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének küldése közvetlen kapcsolatot használ.]]> Alkalmazásadatok biztonsági mentése Az adatbázis előkészítése sikertelen Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. @@ -114,7 +114,7 @@ Az alkalmazásjelkód helyettesítve lesz egy önmegsemmisítő jelkóddal. Arab, bolgár, finn, héber, thai és ukrán – köszönet a felhasználóknak és a Weblate-nek. Engedélyezi a hangüzeneteket? - Mindig legyen használva továbbítókiszolgáló + Mindig legyen használva átjátszó mindig A hívás már véget ért! Engedélyezés @@ -618,7 +618,7 @@ Azonnal A fájlok és a médiatartalmak küldése le van tiltva! Profil elrejtése - Hogyan használja a saját kiszolgálóit + Útmutató a saját kiszolgálók használatához Csevegési üzenetek gyorsabb megtalálása Téma importálása Hiba történt a téma importálásakor @@ -1135,7 +1135,7 @@ Elküldve A hangüzenetek küldése le van tiltva. Legutóbbi üzenetek előnézetének megjelenítése - Az előre beállított kiszolgáló címe + Előre beállított kiszolgáló címe Időszakos értesítések letiltva! A jelkód módosult! Akkor fut, amikor az alkalmazás meg van nyitva @@ -1198,7 +1198,7 @@ A fájlok és a médiatartalmak küldése le van tiltva. Fájl megosztása… Mentés - továbbítókiszolgálón keresztül + átjátszón keresztül Megosztás megállítása Ön eltávolította őt: %1$s Jelmondat mentése és a csevegés megnyitása @@ -1435,7 +1435,7 @@ A kézbesítési jelentések le vannak tiltva %d csoportban Néhány nem végzetes hiba történt az importáláskor: Köszönet a felhasználóknak a Weblate-en való közreműködésért! - A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. + Az átjátszó kiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. Beállítás a rendszer-hitelesítés helyett. Az üzenetfogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása akkor fejeződik be, amikor az üzenetküldési kiszolgáló online lesz. A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. @@ -1444,7 +1444,7 @@ Jelmondat mentése a beállításokban Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem lesznek elküldve. A második pipa, ami már nagyon hiányzott! ✅ - A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. + Az átjátszó kiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók el lesznek távolítva. A kézbesítési jelentések engedélyezve vannak %d csoportban @@ -1597,7 +1597,7 @@ Feltöltés megerősítése Hiba történt az adatbázis törlésekor Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak. - Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-továbbítókiszolgálókra. + Az összes partnere, -beszélgetése és -fájlja biztonságosan titkosítva lesz, majd töredékekre bontva feltöltődnek a beállított XFTP-átjátszókra. Alkalmazásadatok átköltöztetése Adatbázis archiválása Átköltöztetés visszavonása @@ -1756,7 +1756,7 @@ IP-cím védelme Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van). Ismeretlen kiszolgálók! - Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára:\n%1$s. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-átjátszók számára:\n%1$s. Összes színmód Fekete Színmód @@ -1788,7 +1788,7 @@ További kiemelőszín 2 Alkalmazás témája Perzsa kezelőfelület - Védje az IP-címét a partnerei által kiválasztott üzenetváltási továbbítókiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben. + Védje az IP-címét a partnerei által kiválasztott üzenetváltási átjátszókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben. Ismeretlen kiszolgálókról származó fájlok megerősítése. Továbbfejlesztett üzenetkézbesítés Alkalmazás témájának visszaállítása @@ -1932,17 +1932,17 @@ Letiltás Letiltva Stabil - Hiba történt a(z) %1$s továbbítókiszolgálóhoz való kapcsolódáskor. Próbálja meg később. - A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbítókiszolgálóval. - A(z) %1$s továbbítókiszolgáló nem tudott kapcsolódni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. - A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbítókiszolgáló beállításaival. + Hiba történt a(z) %1$s továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbító kiszolgálóval. + A(z) %1$s továbbító kiszolgáló nem tudott kapcsolódni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. + A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbító kiszolgáló beállításaival. Médiatartalom elhomályosítása Közepes Kikapcsolva Enyhe Erős - A továbbítókiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. - A továbbítókiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. hívás A partner törölve lesz – ez a művelet nem vonható vissza! Csak a beszélgetés törlése @@ -2498,10 +2498,10 @@ Tiszta hivatkozás megnyitása Teljes hivatkozás megnyitása Nyomonkövetési paraméterek eltávolítása a hivatkozásokból - SimpleX továbbítókiszolgáló-hivatkozás + SimpleX-átjátszócím Hiba a csevegés olvasottként való megjelölésekor A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. - A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. + A továbbító kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. A kiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. nincs feliratkozás Ön nem kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál (nincs feliratkozás). @@ -2522,7 +2522,117 @@ Hangüzenetek keresése Videók Hangüzenetek - Nem sikerült létrehozni a kapcsolatot + NEM SIKERÜLT LÉTREHOZNI A KAPCSOLATOT sikertelen - Ha csatornákat hozott létre vagy csatlakozott hozzájuk, akkor azok véglegesen le fognak állni. + Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni. + aktív + Közvetítés… + Mégse + csatorna + Csatorna + Csatorna + Csatornahivatkozás + Csatornatagok + Csatorna neve + Kapcsolódás + kapcsolódott + kapcsolódás + Csatorna létrehozása + Csatorna létrehozása + Csatorna létrehozása (béta) + Csatorna létrehozása + törölve + sikertelen + sikertelen + Hivatkozás + új + meghíva + Csatorna megnyitása + Új csatorna megnyitása + TULAJDONOS + Tulajdonosok + Csatorna elhagyása + Elhagyja a csatornát? + Ellenőrzés + Várakozás + Várakozás a válaszra + Ön + Saját csatorna + Saját csatorna + FELIRATKOZÓ + Feliratkozók + %1$d feliratkozó + %1$d feliratkozó + elfogadva + Csatorna törlése + Törli a csatornát? + Csatlakozás a csatornához + Koppintson a „Csatlakozás a csatornához” gombra + A hangrögzítés nem támogatott az Ön által használt eszközön + Nincsenek engedélyezve csevegési átjátszók. + Kiszolgáló-figyelmeztetés + Ön feliratkozó + Ön nem fog több üzenetet kapni ebből a csatornából. A csevegési előzmények megmaradnak. + átjátszó + A csatorna az összes feliratkozó számára törölve lesz – ez a művelet nem vonható vissza! + A csatorna törölve lesz az Ön számára – ez a művelet nem vonható vissza! + Csatornaprofil szerkesztése + Megoszthat egy hivatkozást vagy egy QR-kódot – bárki képes lesz csatlakozni a csatornához. + Csevegési átjátszók + Eltávolítja a feliratkozót? + A feliratkozó el lesz távolítva a csatornából – ez a művelet nem vonható vissza! + Csevegési átjátszó + Új csevegési átjátszó + Előre beállított átjátszó neve + Előre beállított átjátszó címe + Saját átjátszó neve + Saját átjátszó címe + Adja meg az átjátszó nevét… + Átjátszó használata + Átjátszó tesztelése + Használat új csatornákhoz + Átjátszó törlése + Nem sikerült tesztelni az átjátszót! + Hivatkozás megtekintése + Hivatkozás dekódolása + A teszt a(z) %s. lépésnél sikertelen volt. + A kiszolgáló hitelesítést igényel az átjátszóhoz való kapcsolódáshoz, ellenőrizze a jelszavát. + Érvénytelen átjátszónév! + Ellenőrizze az átjátszó nevét, és próbálja újra. + Érvénytelen átjátszócím! + Ellenőrizze az átjátszó címét, és próbálja újra. + Hiba az átjátszó hozzáadásakor + Csevegési átjátszók + A csevegési átjátszók továbbítják az üzeneteket az Ön által létrehozott csatornákban. + Csevegési átjátszók + Nincsenek csevegési átjátszók + A csevegési átjátszók továbbítják az üzeneteket a csatorna feliratkozóinak. + %1$d/%2$d átjátszó aktív, %3$d sikertelen + %1$d/%2$d átjátszó aktív + %1$d/%2$d átjátszó kapcsolódva, %3$d hiba + %1$d/%2$d átjátszó kapcsolódva + ÁTJÁTSZÓ + Átjátszóhivatkozás + Átjátszócím + a következőn keresztül: %1$s + Átjátszó címének megosztása + A feliratkozók az átjátszó hivatkozását használják a csatornához való kapcsolódáshoz.\nAz átjátszócím ennek az átjátszónak a beállítására szolgált a csatornához. + Ön ezen az átjátszóhivatkozáson keresztül kapcsolódott a csatornához. + Feliratkozó eltávolítása + Az összes feliratkozó számára letiltja a feliratkozót? + Hiba a csatorna létrehozásakor + Visszavonja a csatorna létrehozását? + Engedélyezzen legalább egy csevegési átjátszót a csatorna létrehozásához. + A saját profilja (%1$s) meg lesz osztva a csatorna átjátszóival és feliratkozóival. + Átjátszók konfigurálása + Nem sikerült kapcsolódni az átjátszóhoz + Nem minden átjátszó kapcsolódott + Folytatás + A csatorna %2$d átjátszóból %1$d használatával kezd el működni. Folytatja? + Átjátszócím + Ez egy csevegési átjátszó címe, nem használható kapcsolódásra. + %1$s nevű csatornához!]]> + Hiba a csatorna megnyitásakor + Az összes feliratkozó számára feloldja a feliratkozó letiltását? + Átjátszó tesztelése a nevének lekéréséhez.]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_circle_filled.svg new file mode 100644 index 0000000000..c88692fc12 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_circle_filled.svg @@ -0,0 +1 @@ + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_padded.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_padded.svg deleted file mode 100644 index 9f4edcfd98..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bigtop_updates_padded.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code_scanner.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code_scanner.svg new file mode 100644 index 0000000000..6d012c8956 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code_scanner.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 fe4a658a68..b6b372da7c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -2534,7 +2534,7 @@ Apri link pulito Apri link completo Rimuovi il tracciamento del link - Link del relay SimpleX + Indirizzo del relay SimpleX Errore nel segnare la lettura L\'impronta digitale nell\'indirizzo del server di destinazione non corrisponde al certificato: %1$s. L\'impronta digitale nell\'indirizzo del server di inoltro non corrisponde al certificato: %1$s. @@ -2558,7 +2558,117 @@ Video Messaggi vocali Filtro - Connessione fallita + CONNESSIONE FALLITA fallito Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente. + %1$d/%2$d relay attivo/i + %1$d/%2$d relay attivo/i, %3$d fallito/i + %1$d/%2$d relay connesso/i + %1$d/%2$d relay connesso/i, %3$d errori + %1$d iscritto + %1$d iscritti + accettato + attivo + Bloccare l\'iscritto per tutti? + Annulla + Annullare la creazione del canale? + Prova il relay per recuperare il suo nome.]]> + %1$s!]]> + canale + Canale + Canale + Link del canale + Membri del canale + Nome del canale + Il canale verrà eliminato per tutti gli iscritti, non è reversibile! + Il canale verrà eliminato per te, non è reversibile! + Il canale sarà operativo con %1$d di %2$d relay. Procedere? + Relay della chat + Relay della chat + Relay della chat + Relay della chat + I relay della chat inoltrano i messaggi nei canali che crei. + I relay della chat inoltrano i messaggi agli iscritti del canale. + Controlla l\'indirizzo del relay e riprova. + Controlla il nome del relay e riprova. + Configura i relay + Connetti + connesso + in connessione + Crea canale + Crea canale + Crea canale (BETA) + Creazione canale + Decodifica il link + Elimina canale + Eliminare il canale? + eliminato + Elimina relay + Modifica profilo canale + Attiva almeno un relay della chat per creare un canale. + Inserisci il nome del relay… + Errore di aggiunta del relay + Errore di creazione del canale + Errore di apertura del canale + fallito + fallito + Ottieni link + Indirizzo del relay non valido! + Nome del relay non valido! + invitato + Link + nuovo + Nuovo relay di chat + Nessun relay di chat + Nessun relay di chat attivato. + Non tutti i relay sono connessi + Apri canale + Apri un canale nuovo + PROPRIETARIO + Proprietari + Indirizzo relay preimpostato + Nome relay preimpostato + Procedi + relay + RELAY + Indirizzo del relay + Indirizzo del relay + Connessione del relay fallita + Link del relay + Prova del relay fallita! + Rimuovi iscritto + Rimuovere l\'iscritto? + Il server richiede l\'autorizzazione per connettersi al relay, controlla la password. + Avviso del server + Condividi l\'indirizzo del relay + ISCRITTO + Iscritti + Gli iscritti usano il link del relay per connettersi al canale.\nL\'indirizzo del relay è stato usato per impostare questo relay per il canale. + L\'iscritto verrà rimosso dal canale, non è reversibile! + Prova fallita al passo %s. + Prova relay + Questo è un indirizzo di relay di chat, non può essere usato per connettersi. + Sbloccare l\'iscritto per tutti? + Usa per canali nuovi + Usa relay + Verifica + via %1$s + La registrazione vocale non è supportata sulla tua piattaforma + Attendi + Attendi risposta + tu + sei iscritto/a + Ti sei connesso/a al canale attraverso questo link del relay. + Il tuo canale + Il tuo canale + Il tuo profilo %1$s verrà condiviso con i relay del canale e gli iscritti. + L\'indirizzo del tuo relay + Il nome del tuo relay + Smetterai di ricevere messaggi da questo canale. La cronologia della chat sarà preservata. + Iscriviti al canale + Esci dal canale + Uscire dal canale? + Tocca Iscriviti al canale + Puoi condividere un link o un codice QR, chiunque sarà in grado di iscriversi al canale. + Trasmetti 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 a67f00a459..be037e8b87 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 @@ -2208,12 +2208,12 @@ 离开聊天? 你将停止从这个聊天收到消息。聊天历史将被保留。 邀请加入聊天 - 将为你删除聊天 - 此操作无法撤销! + 将为你删除聊天 —— 此操作无法撤销! 删除聊天 删除聊天? 添加好友 添加团队成员 - 将为所有成员删除聊天 - 此操作无法撤销! + 将为所有成员删除聊天 —— 此操作无法撤销! 仅聊天所有人可更改首选项。 角色将被更改为 %s。聊天中的每个人都会收到通知。 成员之间的私信被禁止。 @@ -2518,7 +2518,7 @@ 打开干净链接 打开完整链接 删除链接跟踪 - SimpleX 中继链接 + SimpleX 中继地址 标记为已读时出错 目标服务器地址的指纹和证书不匹配:%1$s。 转发服务器地址的指纹和证书不匹配:%1$s。 @@ -2545,4 +2545,114 @@ 连接失败 失败 如果你加入了或创建了频道,它们会永远停止工作。 + 订阅者使用中继链接连接到频道。\n中继地址用于为频道设置这个中继。 + %1$d 个中继活跃,共 %2$d 个 + %1$d 个中继活跃,共 %2$d 个,%3$d 个失灵 + %1$d 个中继已连接,共 %2$d 个 + %1$d 个中继已连接,共 %2$d 个,%3$d 个出错 + %1$d 位订阅者 + %1$d 位订阅者 + 已接受 + 活跃 + 为所有人拦截订阅者? + 广播 + 取消 + 取消创建频道? + 测试中继 来获取其名称。]]> + %1$s 频道的链接!]]> + 频道 + 频道 + 频道 + 频道链接 + 频道成员 + 频道名 + 将为所有订阅者删除频道 —— 此操作无法撤销! + 将为你删除频道 —— 此操作无法撤销! + 频道将开始用 %2$d 个中继中的 %1$d 个中继运作。要继续吗? + 聊天中继 + 聊天中继 + 聊天中继 + 聊天中继 + 聊天中继在你创建的频道中转发消息。 + 聊天中继转发消息给频道订阅者。 + 检查中继地址并重试。 + 检查中继名并重试。 + 配置中继 + 连接 + 已连接 + 正在连接 + 创建频道 + 创建频道 + 创建频道(测试版) + 正在创建频道 + 解码链接 + 删除频道 + 删除频道吗? + 已删除 + 删除中继 + 编辑频道简介 + 要创建频道至少启用一个聊天中继。 + 输入中继名… + 添加中继出错 + 创建频道出错 + 打开频道出错 + 失败 + 失败 + 获取链接 + 无效的中继地址! + 无效的中继名! + 已邀请 + 加入频道 + 离开频道 + 离开频道? + 链接 + + 新聊天中继 + 无聊天中继 + 未启用聊天中继。 + 不是所有中继均已连接 + 打开频道 + 打开新频道 + 所有者 + 所有者 + 预设中继地址 + 预设中继名 + 继续 + 中继 + 中继 + 中继地址 + 中继地址 + 中继连接失败 + 中继链接 + 中继测试失败! + 删除订阅者 + 删除订阅者? + 服务器需要身份认证来连接到中继,检查密码。 + 服务器警告 + 分享中继地址 + 订阅者 + 订阅者 + 将从频道删除订阅者 —— 此操作无法撤销! + 轻触加入频道 + 测试在第 %s 步失败。 + 测试中继 + 这是聊天中继地址,无法用于连接。 + 为所有人解封订阅者? + 用于新频道 + 使用中继 + 验证 + 通过 %1$s + 你的平台不支持录音 + 等待 + 等待响应 + + 你是订阅者 + 可以分享链接或二维码 —— 任何人均可加入该频道。 + 你通过此中继链接连接至该频道。 + 你的频道 + 你的频道 + 你的个人资料 %1$s 将分享给频道中继和订阅者。 + 你的中继地址 + 你的中继名 + 你会停止收到来自该频道的消息。聊天记录将被保留。 diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_create_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/banner_paste_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_connect_via_link_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_create_your_public_address_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_invite_someone_privately_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/card_let_someone_connect_to_you_alpha_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/connect_via_link_small_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/create_group_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/one_time_link_small_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_light.svg new file mode 100644 index 0000000000..2325330d90 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/simplex_address_small_light.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt index 18b17b25a9..0d5ec24bff 100644 --- a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt @@ -18,8 +18,7 @@ class ChatItemsMergerTest { val chatState1 = ActiveChatState(splits = splits1) val removed1 = listOf(oldItems[1]) val newItems1 = oldItems - removed1 - val recalc1 = recalculateChatStatePositions(chatState1) - recalc1.removed(removed1.map { Triple(it.id, oldItems.indexOf(removed1[0]), it.isRcvNew) }, newItems1) + chatState1.itemsRemoved(removed1.map { Triple(it.id, oldItems.indexOf(removed1[0]), it.isRcvNew) }, newItems1) assertEquals(1, splits1.value.size) assertEquals(124L, splits1.value.first()) @@ -27,8 +26,7 @@ class ChatItemsMergerTest { val chatState2 = ActiveChatState(splits = splits2) val removed2 = listOf(oldItems[1], oldItems[2]) val newItems2 = oldItems - removed2 - val recalc2 = recalculateChatStatePositions(chatState2) - recalc2.removed(removed2.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed2[index]), it.isRcvNew) }, newItems2) + chatState2.itemsRemoved(removed2.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed2[index]), it.isRcvNew) }, newItems2) assertEquals(1, splits2.value.size) assertEquals(125L, splits2.value.first()) @@ -36,14 +34,12 @@ class ChatItemsMergerTest { val chatState3 = ActiveChatState(splits = splits3) val removed3 = listOf(oldItems[1], oldItems[2], oldItems[3]) val newItems3 = oldItems - removed3 - val recalc3 = recalculateChatStatePositions(chatState3) - recalc3.removed(removed3.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed3[index]), it.isRcvNew) }, newItems3) + chatState3.itemsRemoved(removed3.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed3[index]), it.isRcvNew) }, newItems3) assertEquals(0, splits3.value.size) val splits4 = MutableStateFlow(listOf(123L)) val chatState4 = ActiveChatState(splits = splits4) - val recalc4 = recalculateChatStatePositions(chatState4) - recalc4.cleared() + chatState4.clear() assertEquals(0, splits4.value.size) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt index 2a1a26df95..e4866c845d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/StoreWindowState.kt @@ -3,11 +3,12 @@ package chat.simplex.common import chat.simplex.common.model.json import chat.simplex.common.platform.appPreferences import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.ui.theme.DEFAULT_WINDOW_WIDTH import kotlinx.serialization.* @Serializable data class WindowPositionSize( - val width: Int = 1366, + val width: Int = DEFAULT_WINDOW_WIDTH.value.toInt(), val height: Int = 768, val x: Int = 0, val y: Int = 0, 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 c5a38ec4a1..f88c539284 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 @@ -265,7 +265,9 @@ actual class VideoPlayer actual constructor( mediaPlayer().events().addMediaPlayerEventListener(object: MediaPlayerEventAdapter() { override fun mediaPlayerReady(mediaPlayer: MediaPlayer?) { playerThread.execute { - mediaPlayer?.audio()?.setVolume(100) + // Do not call setVolume here: on Windows VLCJ routes it through WASAPI ISimpleAudioVolume, + // which resets SimpleX Chat's per-app volume in the Windows Volume Mixer on every playback + // (VLCJ issue #985). A fresh VLCJ MediaPlayer already defaults to volume 100, so this was redundant. mediaPlayer?.audio()?.isMute = false } } diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 3446ef9c30..45129a0d54 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5-beta.8 -android.version_code=340 +android.version_name=6.5-beta.9 +android.version_code=341 android.bundle=false -desktop.version_name=6.5-beta.8 -desktop.version_code=135 +desktop.version_name=6.5-beta.9 +desktop.version_code=136 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 diff --git a/apps/multiplatform/local.properties.example b/apps/multiplatform/local.properties.example index 8fa9a47963..9aa560d839 100644 --- a/apps/multiplatform/local.properties.example +++ b/apps/multiplatform/local.properties.example @@ -3,6 +3,8 @@ enable_debuggable=true application_id.suffix=.debug app.name=SimpleX Debug +#simplex.assets.dir=path/to/assets + #desktop.mac.signing.identity=SimpleX Chat Ltd #desktop.mac.signing.keychain=/path/to/simplex.keychain #desktop.mac.notarization.apple_id=example@example.com diff --git a/apps/multiplatform/settings.gradle.kts b/apps/multiplatform/settings.gradle.kts index 40446f1958..50a50d531d 100644 --- a/apps/multiplatform/settings.gradle.kts +++ b/apps/multiplatform/settings.gradle.kts @@ -3,7 +3,6 @@ pluginManagement { google() gradlePluginPortal() mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } plugins { diff --git a/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md b/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md new file mode 100644 index 0000000000..39616304dd --- /dev/null +++ b/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md @@ -0,0 +1,38 @@ +--- +layout: layouts/article.html +title: "SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech" +date: 2026-04-28 +# previewBody: blog_previews/20260421.html +# image: images/20260421-channel.png +# imageBottom: true +draft: true +permalink: "/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html" +--- + +# SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech + +**To be published:** Apr 28, 2026 + +This is a permalink for a blog post about: + +- SimpleX Channels - a new model for online publishing that preserves participation privacy, protecting both user and network operators. It is being released in v6.5 +- SimpleX Network Consortium - a cross-jurisdictional governance and licensing structure to ensure long term availability and sustainability of SimpleX Network. +- Testing the water for community crowdfunding under Reg CF. + +## SimpleX Channels - more public, more freedom, more private + +TODO + +## SimpleX Network Consortium - to govern SimpleX Network + +TODO + +## Community Crowdfunding + +TODO + +*Register your interest* to participate in crowdfunding here: https://simplexchat.typeform.com/crowdfunding + +Join the channel for updates here: https://smp4.simplex.im/g#g6pdBGlLoeOwqYmbmyvRye8EBiFd2inNUzKc87Pt3y4 + +_Disclaimer: SimpleX Chat is testing the waters for a possible Reg CF offering. We’re not asking for or accepting any money right now, and we won’t accept any if sent. We can’t accept any offers to buy securities or take any payments until the official filing is done and it’s live through a regulated platform. Our testing the waters and your possible indications of interest doesn’t create any obligation or commitment of any kind._ diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index b5d49077c0..ab3ec3d241 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -983,6 +983,11 @@ PublicGroupCreated: Public group created. - groupLink: [GroupLink](./TYPES.md#grouplink) - groupRelays: [[GroupRelay](./TYPES.md#grouprelay)] +PublicGroupCreationFailed: Public group creation failed. +- type: "publicGroupCreationFailed" +- user: [User](./TYPES.md#user) +- addRelayResults: [[AddRelayResult](./TYPES.md#addrelayresult)] + ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1280,6 +1285,7 @@ Determine SimpleX link type and if the bot is already connected via this link. **Parameters**: - userId: int64 - connectionLink: string? +- linkOwnerSig: [LinkOwnerSig](./TYPES.md#linkownersig)? **Syntax**: diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index aee16ac5ea..23fc79b634 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -5,6 +5,7 @@ This file is generated automatically. - [ACIReaction](#acireaction) - [AChat](#achat) - [AChatItem](#achatitem) +- [AddRelayResult](#addrelayresult) - [AddressSettings](#addresssettings) - [AgentCryptoError](#agentcryptoerror) - [AgentErrorType](#agenterrortype) @@ -117,6 +118,7 @@ This file is generated automatically. - [InvitationLinkPlan](#invitationlinkplan) - [InvitedBy](#invitedby) - [LinkContent](#linkcontent) +- [LinkOwnerSig](#linkownersig) - [LinkPreview](#linkpreview) - [LocalProfile](#localprofile) - [MemberCriteria](#membercriteria) @@ -132,6 +134,7 @@ This file is generated automatically. - [NetworkError](#networkerror) - [NewUser](#newuser) - [NoteFolder](#notefolder) +- [OwnerVerification](#ownerverification) - [PendingContactConnection](#pendingcontactconnection) - [PrefEnabled](#prefenabled) - [Preferences](#preferences) @@ -218,6 +221,15 @@ This file is generated automatically. - chatItem: [ChatItem](#chatitem) +--- + +## AddRelayResult + +**Record type**: +- relay: [UserChatRelay](#userchatrelay) +- relayError: [ChatError](#chaterror)? + + --- ## AddressSettings @@ -1698,6 +1710,7 @@ Error: Ok: - type: "ok" - contactSLinkData_: [ContactShortLinkData](#contactshortlinkdata)? +- ownerVerification: [OwnerVerification](#ownerverification)? OwnLink: - type: "ownLink" @@ -2261,6 +2274,7 @@ Ok: - type: "ok" - groupSLinkInfo_: [GroupShortLinkInfo](#groupshortlinkinfo)? - groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? +- ownerVerification: [OwnerVerification](#ownerverification)? OwnLink: - type: "ownLink" @@ -2277,6 +2291,10 @@ Known: - type: "known" - groupInfo: [GroupInfo](#groupinfo) +NoRelays: +- type: "noRelays" +- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)? + --- @@ -2526,6 +2544,7 @@ Public: Ok: - type: "ok" - contactSLinkData_: [ContactShortLinkData](#contactshortlinkdata)? +- ownerVerification: [OwnerVerification](#ownerverification)? OwnLink: - type: "ownLink" @@ -2578,6 +2597,16 @@ Unknown: - json: JSONObject +--- + +## LinkOwnerSig + +**Record type**: +- ownerId: string? +- chatBinding: string +- ownerSig: string + + --- ## LinkPreview @@ -2683,6 +2712,7 @@ Chat: - type: "chat" - text: string - chatLink: [MsgChatLink](#msgchatlink) +- ownerSig: [LinkOwnerSig](#linkownersig)? Unknown: - type: "unknown" @@ -2830,6 +2860,20 @@ SubscribeError: - unread: bool +--- + +## OwnerVerification + +**Discriminated union type**: + +Verified: +- type: "verified" + +Failed: +- type: "failed" +- reason: string + + --- ## PendingContactConnection @@ -3242,6 +3286,7 @@ ParseError: - "invited" - "accepted" - "active" +- "inactive" --- diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 26b48e56b0..ae8ce7c05b 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -117,7 +117,7 @@ chatCommandsDocsData = ("APILeaveGroup", [], "Leave group.", ["CRLeftMemberUser", "CRChatCmdError"], [], Just UNBackground, "/_leave #" <> Param "groupId"), ("APIListMembers", [], "Get group members.", ["CRGroupMembers", "CRChatCmdError"], [], Nothing, "/_members #" <> Param "groupId"), ("APINewGroup", [], "Create group.", ["CRGroupCreated", "CRChatCmdError"], [], Nothing, "/_group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Json "groupProfile"), - ("APINewPublicGroup", [], "Create public group.", ["CRPublicGroupCreated", "CRChatCmdError"], [], Just UNInteractive, "/_public group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Join ',' "relayIds" <> " " <> Json "groupProfile"), + ("APINewPublicGroup", [], "Create public group.", ["CRPublicGroupCreated", "CRPublicGroupCreationFailed", "CRChatCmdError"], [], Just UNInteractive, "/_public group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Join ',' "relayIds" <> " " <> Json "groupProfile"), ("APIGetGroupRelays", [], "Get group relays.", ["CRGroupRelays", "CRChatCmdError"], [], Nothing, "/_get relays #" <> Param "groupId"), ("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated", "CRChatCmdError"], [], Just UNBackground, "/_group_profile #" <> Param "groupId" <> " " <> Json "groupProfile") ] @@ -282,6 +282,7 @@ cliCommands = "SetUserGroupReceipts", "SetUserAutoAcceptMemberContacts", "SetUserTimedMessages", + "SharePublicGroup", "ShowChatItem", "ShowChatItemInfo", "ShowGroupDescription", @@ -407,6 +408,7 @@ undocumentedCommands = "APISetUserGroupReceipts", "APISetUserServers", "APISetUserUIThemes", + "APIShareChatMsgContent", "APIStandaloneFileInfo", "APIStorageEncryption", "APISuspendChat", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 873ca5eb97..c3ab85ece6 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -69,6 +69,7 @@ chatResponsesDocsData = ("CRGroupLinkDeleted", ""), ("CRGroupCreated", ""), ("CRPublicGroupCreated", ""), + ("CRPublicGroupCreationFailed", ""), ("CRGroupRelays", ""), ("CRGroupMembers", ""), ("CRGroupUpdated", ""), @@ -132,6 +133,7 @@ undocumentedResponses = "CRChatItemInfo", "CRChatItems", "CRChatItemTTL", + "CRChatMsgContent", "CRChatRelayTestResult", "CRChats", "CRConnectionsDiff", diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 826b8c1957..e2f67c88c6 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -202,6 +202,7 @@ chatTypesDocsData = (sti @(ContactUserPref SimplePreference), STUnion, "CUP", [], "", ""), (sti @(ContactUserPreference SimplePreference), STRecord, "", [], "", ""), (sti @(CreatedConnLink 'CMContact), STRecord, "", [], Param "connFullLink" <> Optional "" (" " <> Param "$0") "connShortLink", ""), + (sti @AddRelayResult, STRecord, "", [], "", ""), (sti @AddressSettings, STRecord, "", [], "", ""), (sti @AgentCryptoError, STUnion, "", ["RATCHET_EARLIER", "RATCHET_SKIPPED"], "", ""), -- TODO add fields to types (sti @AgentErrorType, STUnion, "", [], "", ""), @@ -299,6 +300,7 @@ chatTypesDocsData = (sti @InvitationLinkPlan, STUnion, "ILP", [], "", ""), (sti @InvitedBy, STUnion, "IB", [], "", ""), (sti @LinkContent, STUnion, "LC", [], "", ""), + (sti @LinkOwnerSig, STRecord, "", [], "", ""), (sti @LinkPreview, STRecord, "", [], "", ""), (sti @LocalProfile, STRecord, "", [], "", ""), (sti @MemberCriteria, STEnum1, "MC", [], "", ""), @@ -314,6 +316,7 @@ chatTypesDocsData = (sti @NetworkError, STUnion, "NE", [], "", ""), (sti @NewUser, STRecord, "", [], "", ""), (sti @NoteFolder, STRecord, "", [], "", ""), + (sti @OwnerVerification, STUnion, "OV", [], "", ""), (sti @PendingContactConnection, STRecord, "", [], "", ""), (sti @PrefEnabled, STRecord, "", [], "", ""), (sti @Preferences, STRecord, "", [], "", ""), @@ -403,6 +406,7 @@ deriving instance Generic (CIReaction c d) deriving instance Generic (ContactUserPref p) deriving instance Generic (ContactUserPreference p) deriving instance Generic (CreatedConnLink m) +deriving instance Generic AddRelayResult deriving instance Generic AddressSettings deriving instance Generic AgentCryptoError deriving instance Generic AgentErrorType @@ -506,6 +510,7 @@ deriving instance Generic JSONCIDirection deriving instance Generic JSONCIFileStatus deriving instance Generic JSONCIStatus deriving instance Generic LinkContent +deriving instance Generic LinkOwnerSig deriving instance Generic LinkPreview deriving instance Generic LocalProfile deriving instance Generic MemberCriteria @@ -521,6 +526,7 @@ deriving instance Generic MsgSigStatus deriving instance Generic NetworkError deriving instance Generic NewUser deriving instance Generic NoteFolder +deriving instance Generic OwnerVerification deriving instance Generic PendingContactConnection deriving instance Generic PrefEnabled deriving instance Generic Preferences diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index 37f74e4275..36e87db62d 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -214,6 +214,7 @@ toTypeInfo tr = "ProtocolServer", "SbKey", "SharedMsgId", + "Signature", "TransportHost", "UIColor", "UserPwd", diff --git a/cabal.project b/cabal.project index 65983d22a5..557dee3951 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: bc5ea42bec3a63e46b191e4150dd79957f114e01 + tag: 858fac7f4f821a2df6fbea03a1bfbb82ea9717c5 source-repository-package type: git diff --git a/docs/ANDROID.md b/docs/ANDROID.md index 61f81d1a40..d0422e1abd 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -49,7 +49,7 @@ Please, note, that if you use a modern version of SimpleX, the databases will be In order to view database data you need to decrypt it first. Install `sqlcipher` using your favorite package manager and run the following commands in the directory with databases: ```bash sqlcipher files_chat.db -pragma key="youDecryptionPassphrase"; +pragma key="yourDecryptionPassphrase"; # Ensure it works fine select * from users; ``` diff --git a/docs/BUSINESS.md b/docs/BUSINESS.md index 8fd5df5c36..b72bf00257 100755 --- a/docs/BUSINESS.md +++ b/docs/BUSINESS.md @@ -9,7 +9,7 @@ SimpleX Chat (aka SimpleX) is a decentralized communication network that provide This document aims to help you make the best use of SimpleX Chat if you choose to engage with its users. -## Communcate with customers via business address +## Communicate with customers via business address In the same way you can connect to our "SimpleX Chat team" profile via the app, you can provide the address for your existing and prospective customers: - to buy your product and services via chat, @@ -85,7 +85,7 @@ To install SimpleX Chat CLI in the cloud, follow this: simplex-chat ``` -To deattach from running CLI simply press `Ctrl+B` and then `D`. +To detach from a running CLI, simply press `Ctrl+B` and then `D`. To reattach back to CLI, run: `tmux attach -t simplex-cli`. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6ae5418d0a..3ecfa17409 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -21,7 +21,7 @@ Please discuss the problem you want to solve and your detailed implementation pl ./contributing/CODE.md has details about general requirements common for `simplexmq` and `simplex-chat` repositories. -This files can be used with LLM prompts, e.g. if you use Claude Code you can create CLAUDE.md file in project root importing content from these files: +These files can be used with LLM prompts, e.g. if you use Claude Code you can create CLAUDE.md file in project root importing content from these files: ```markdown @README.md @@ -71,7 +71,7 @@ You will have to add `/opt/homebrew/opt/openssl@3.0/bin` to your PATH in order t 1. Make PRs to `master` branch _only_ for both simplex-chat and simplexmq repos. -2. To build core libraries for Android, iOS and windows: +2. To build core libraries for Android, iOS and Windows: - merge `master` branch to `master-android` branch. - push to GitHub. diff --git a/docs/DIRECTORY.md b/docs/DIRECTORY.md index 8659222280..50e7771a1a 100644 --- a/docs/DIRECTORY.md +++ b/docs/DIRECTORY.md @@ -96,7 +96,7 @@ Group owners are expected to moderate the content in the groups, if members post We reserve the right to not accept the group listing in the directory or cancel its listing, and there may be cases when we can't provide an explanation. We will certainly try to avoid it by communicating with the group owners first. -The combination of display name and full name has to be unique for the listed groups. If a group uses the name or logo of SimpleX, SimpleX network or SimpleX Chat it must be consistent with [Permitted Uses or SimpleX trademark](./TRADEMARK.md). +The combination of display name and full name has to be unique for the listed groups. If a group uses the name or logo of SimpleX, SimpleX network or SimpleX Chat it must be consistent with [Permitted Uses of SimpleX trademark](./TRADEMARK.md). Once the group is listed in the directory, the bot will invite you to join the group of the group owners, where you can send any ideas or suggestions for how the groups functionality should evolve, and help steer both the product and the policies. diff --git a/docs/DONATIONS.md b/docs/DONATIONS.md index 50eb62a401..b3046ee90b 100644 --- a/docs/DONATIONS.md +++ b/docs/DONATIONS.md @@ -7,7 +7,7 @@ permalink: /donate/index.html Huge thank you to everybody who donated to SimpleX Chat! -We are prioritizing users privacy and security - it would be impossible without your support. +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 platform based on the same principles as email and web, but much more private and secure. diff --git a/docs/FAQ.md b/docs/FAQ.md index 401f025d9c..8c14168811 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -51,7 +51,7 @@ revision: 13.08.2025 ### How do I connect to people? -Tap "pencil" button in the right corner, then "Create 1-time link". Share the link with the person you want to connect to. Your contact has to paste the link to the app's search bar. The link will can also be opened via the browser, once the app is installed. +Tap "pencil" button in the right corner, then "Create 1-time link". Share the link with the person you want to connect to. Your contact has to paste the link to the app's search bar. The link can also be opened via the browser, once the app is installed. Alternatively, you can show the QR code when meeting in person or in a video call. @@ -103,7 +103,7 @@ Also see: [I do not see the second tick on the messages I sent](#i-do-not-see-th ### I want to see when my contacts read my messages -To know when your contact read your messages, your contact's app has to send you a confirmation message. And vice versa, for your contact to know when you read the message, your app has to send a confirmation message. +To know when your contact reads your messages, your contact's app has to send you a confirmation message. And vice versa, for your contact to know when you read the message, your app has to send a confirmation message. The important questions for this feature: - do you always want that your contacts can see when you read all their messages? Probably, even with your close friends, sometimes you would prefer to have time before you answer their message, and also have a plausible deniability that you have not seen the message. And this should be ok - in the end, this is your device, and it should be for you to decide whether this confirmation message is sent or not, and when it is sent. @@ -111,7 +111,7 @@ The important questions for this feature: Overall, it seems that this feature is more damaging to your communications with your contacts than it is helpful. It keeps senders longer in the app, nervously waiting for read receipts, exploiting addictive patterns - having you spend more time in the app is the reason why it is usually present in most messaging apps. It also creates a pressure on the recipients to reply sooner, and if read receipts are opt-in, it creates a pressure to enable it, that can be particularly damaging in any relationships with power imbalance. -We think that delivery receipts are important and equally benefit both sides as the conversation, as they confirm that communication network functions properly. But we strongly believe that read receipts is an anti-feature that only benefits the app developers, and hurts the relations between the app users. So we are not planning to add it even as opt-in. In case you want your contact to know you've read the message put a reaction to it. And if you don't want them to know it - it is also ok, what your device sends should be under your control. +We think that delivery receipts are important and equally benefit both sides as the conversation, as they confirm that communication network functions properly. But we strongly believe that read receipts are an anti-feature that only benefits the app developers, and hurts the relations between the app users. So we are not planning to add it even as opt-in. In case you want your contact to know you've read the message put a reaction to it. And if you don't want them to know it - it is also ok, what your device sends should be under your control. ### Can I use the same profile on desktop? Do messages sync cross-platform? @@ -130,7 +130,7 @@ We believe that allowing deleting information from your device to your contacts 2) it may be a business communication, and either your organization policy or a compliance requirement is that every message you receive must be preserved for some time. 3) the message can contain a legally binding promise, effectively a contract between you and your contact, in which case you both need to keep it. 4) the messages may contain threat or abuse and you may want to keep them as a proof. -5) you may have paid for the the message (e.g., it can be a design project or consulting report), and you don't want it to suddenly disappear before you had a chance to store it outside of the conversation. +5) you may have paid for the message (e.g., it can be a design project or consulting report), and you don't want it to suddenly disappear before you had a chance to store it outside of the conversation. It is also important to remember, that even if your contact enabled "Delete for everyone", you cannot really see it as a strong guarantee that the message will be deleted. Your contact's app can have a very simple modification (a one-line code change), that would prevent this deletion from happening when you request it. So you cannot see it as something that guarantees your security from your contacts. @@ -232,7 +232,7 @@ You may not have the second tick on your sent messages for these reasons: ### 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 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_ of 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. @@ -298,7 +298,7 @@ You can resolve it by deleting the app's database: (WARNING: this results in del ### My mobile app does not connect to desktop app 1. Check that both devices are connected to the same network (e.g., it won't work if mobile is connected to mobile Internet and desktop to WiFi). -2. If you use VPN on mobile, allow connections to local network in you VPN settings, or disable VPN. +2. If you use VPN on mobile, allow connections to local network in your VPN settings, or disable VPN. 3. Allow SimpleX Chat on desktop to accept network connections in system firewall settings. You may choose a specific port in desktop app to accept connections, by default it uses a random port every time. 4. Check that your WiFi router allows connections between devices (e.g., it may have an option for "device isolation", or similar). 5. If you see an error "certificate expired", please check that your device clocks are synchronized within a few seconds. @@ -312,7 +312,7 @@ If none of the suggestions work for you, you can create a separate profile on ea ### 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). +Yes! Please read more about how 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). ### Why can't I use the same profile on different devices? @@ -355,7 +355,7 @@ If the servers didn't upgrade, the messages would temporarily fail to deliver. Y With private routing enabled, instead of connecting to your contact's server directly, your client would "instruct" one of the known servers to forward the message, preventing the destination server from observing your IP address. -Your messages are additionally end-to-end encrypted between your client and the destination server, so that the forwarding server cannot observe the destination addresses and server responses – similarly to how onion routing work. Private message routing is, effectively, a two-hop onion packet routing. +Your messages are additionally end-to-end encrypted between your client and the destination server, so that the forwarding server cannot observe the destination addresses and server responses – similarly to how onion routing works. Private message routing is, effectively, a two-hop onion packet routing. Also, this connection is protected from man-in-the-middle attack by the forwarding server, as your client will validate destination server certificate using its fingerprint in the server address. @@ -375,7 +375,7 @@ Private message routing routes packets (each message is one 16kb packet), not so As each message uses its own random encryption key and random (non-sequential) identifier, the destination server cannot link multiple message queue addresses to the same client. At the same time, the forwarding server cannot observe which (and how many) addresses on the destination server your client sends messages to, thanks to e2e encryption between the client and destination server. In that regard, this design is similar to onion routing, but with per-packet anonymity, not per-circuit. -This design is similar to mixnets (e.g. [Nym network](https://nymtech.net)), and it is tailored to the needs of message routing, providing better transport anonymity that general purpose networks, like Tor or VPN. You still can use Tor or VPN to connect to known servers, to protect your IP address from them. +This design is similar to mixnets (e.g. [Nym network](https://nymtech.net)), and it is tailored to the needs of message routing, providing better transport anonymity than general-purpose networks, like Tor or VPN. You still can use Tor or VPN to connect to known servers, to protect your IP address from them. ### Why don't you embed Tor in SimpleX Chat app? diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index fe0bd107ed..c8fdd56bdd 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -147,7 +147,7 @@ SimpleX Clients also form a network using SMP relays and IP or some other overla [Wikipedia](https://en.wikipedia.org/wiki/Overlay_network) -# Non-repudiation +## Non-repudiation The property of the cryptographic or communication system that allows the recipient of the message to prove to any third party that the sender identified by some cryptographic key sent the message. It is the opposite to [repudiation](#repudiation). While in some context non-repudiation may be desirable (e.g., for contractually binding messages), in the context of private communications it may be undesirable. @@ -157,7 +157,7 @@ The property of the cryptographic or communication system that allows the recipi Generalizing [the definition](https://csrc.nist.gov/glossary/term/pairwise_pseudonymous_identifier) from NIST Digital Identity Guidelines, it is an opaque unguessable identifier generated by a service used to access a resource by only one party. -In the context of SimpleX network, these are the identifiers generated by SMP relays to access anonymous messaging queues, with a separate identifier (and access credential) for each accessing party: recipient, sender and and optional notifications subscriber. The same approach is used by XFTP relays to access file chunks, with separate identifiers (and access credentials) for sender and each recipient. +In the context of SimpleX network, these are the identifiers generated by SMP relays to access anonymous messaging queues, with a separate identifier (and access credential) for each accessing party: recipient, sender and an optional notifications subscriber. The same approach is used by XFTP relays to access file chunks, with separate identifiers (and access credentials) for sender and each recipient. ## Peer-to-peer @@ -177,7 +177,7 @@ The quality of the end-to-end encryption scheme allowing to recover security aga ## Post-quantum cryptography -Any of the proposed cryptographic systems or algorithms that are thought to be secure against an attack by a quantum computer. It appears that as of 2025 there is no system or algorithm that is *proven* to be secure against such attacks, or even to be secure against attacks by massively parallel conventional computers, so a general recommendation is to use post-quantum hybrid cryptography - combining post-quantum and traditional algorigthms. +Any of the proposed cryptographic systems or algorithms that are thought to be secure against an attack by a quantum computer. It appears that as of 2025 there is no system or algorithm that is *proven* to be secure against such attacks, or even to be secure against attacks by massively parallel conventional computers, so a general recommendation is to use post-quantum hybrid cryptography - combining post-quantum and traditional algorithms. [Wikipedia](https://en.wikipedia.org/wiki/Post-quantum_cryptography) diff --git a/docs/REPRODUCE.md b/docs/REPRODUCE.md index 03fb6b4336..50c0b99280 100644 --- a/docs/REPRODUCE.md +++ b/docs/REPRODUCE.md @@ -46,7 +46,7 @@ echo -e "trust\n5\ny\nquit" | gpg --command-fd 0 --edit-key build@simplex.chat ## Verify release signature -**Linux dekstop apps and CLI**: +**Linux desktop apps and CLI**: Download the file with executable hashes and the signature. For example, to verify the `v6.5.0-beta.3` release: @@ -149,13 +149,13 @@ To reproduce the build you must have: The script executes these steps (please review the script to confirm): - 1) builds all Linux CLI and Dekstop binaries for the release in docker container. + 1) builds all Linux CLI and Desktop binaries for the release in docker container. 2) downloads binaries from the same GitHub release and compares them with the built binaries. 3) if they all match, generates _sha256sums file with their checksums. This will take a while. -4. After compilation, you should see the folder named as the tag and reprository name (e.g., `v6.4.8-simplex-chat`) with two subfolders: +4. After compilation, you should see the folder named as the tag and repository name (e.g., `v6.4.8-simplex-chat`) with two subfolders: ```sh ls v6.4.8-simplex-chat @@ -169,7 +169,7 @@ To reproduce the build you must have: ### Android apps -In addition to basic requirments, Android build will: +In addition to basic requirements, Android build will: - Take ~150gb of disc space - Take ~20h to build all the architectures (depends on core count) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 72db650c35..b9218fbebc 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -54,7 +54,7 @@ We will determine the risk of each issue, taking into account our experience dea **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. +- **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. diff --git a/docs/SERVER.md b/docs/SERVER.md index f45403be8a..a35ede5cd0 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -59,7 +59,7 @@ To create SMP server as a systemd service, you'll need: - Your server domain, with A and AAAA records specifying server IPv4 and IPv6 addresses (`smp1.example.com`) - A basic Linux knowledge. -*Please note*: while you can run an SMP server without a domain name, in the near future client applications will start using server domain name in the invitation links (instead of `simplex.chat` domain they use now). In case a server does not have domain name and server pages (see below), the clients will be generaing the links with `simplex:` scheme that cannot be opened in the browsers. +*Please note*: while you can run an SMP server without a domain name, in the near future client applications will start using server domain name in the invitation links (instead of `simplex.chat` domain they use now). In case a server does not have domain name and server pages (see below), the clients will be generating the links with `simplex:` scheme that cannot be opened in the browsers. 1. Install server with [Installation script](https://github.com/simplex-chat/simplexmq#using-installation-script). @@ -82,7 +82,7 @@ To create SMP server as a systemd service, you'll need: --control-port \ --socks-proxy \ --source-code \ - --fqdn=smp1.example.com + --fqdn=smp1.example.com' ``` 4. Install tor: @@ -114,7 +114,7 @@ To create SMP server as a systemd service, you'll need: ```sh # Enable log (otherwise, tor doesn't seem to deploy onion address) Log notice file /var/log/tor/notices.log - # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonimity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, keep standard configuration. + # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonymity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, keep standard configuration. SOCKSPort 0 HiddenServiceNonAnonymousMode 1 HiddenServiceSingleHopMode 1 @@ -194,12 +194,12 @@ To create SMP server as a systemd service, you'll need: key_name='web.key' cert_name='web.crt' - # Copy certifiacte from Caddy directory to smp-server directory + # Copy certificate from Caddy directory to smp-server directory cp "${folder_in}/${domain}.crt" "${folder_out}/${cert_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${cert_name}" - # Copy certifiacte key from Caddy directory to smp-server directory + # Copy certificate key from Caddy directory to smp-server directory cp "${folder_in}/${domain}.key" "${folder_out}/${key_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${key_name}" @@ -535,7 +535,7 @@ To verify server binaries after you downloaded them: > Good signature from "SimpleX Chat " -5. Compute the hashes of the binaries you plan to use with `shu256sum ` or with `openssl sha256 ` and compare them with the hashes in the file `_sha256sums` - they must be the same. +5. Compute the hashes of the binaries you plan to use with `sha256sum ` or with `openssl sha256 ` and compare them with the hashes in the file `_sha256sums` - they must be the same. That is it - you now verified authenticity of our GitHub server binaries. @@ -634,7 +634,7 @@ to initialize your `smp-server` configuration with: --- -After that, your installation is complete and you should see in your teminal output something like this: +After that, your installation is complete and you should see in your terminal output something like this: ```sh Certificate request self-signature ok @@ -742,7 +742,7 @@ websockets: off [PROXY] # Network configuration for SMP proxy client. # `host_mode` can be 'public' (default) or 'onion'. -# It defines prefferred hostname for destination servers with multiple hostnames. +# It defines preferred hostname for destination servers with multiple hostnames. # host_mode: public # required_host_mode: off @@ -757,7 +757,7 @@ websockets: off # or 'always' to be used for all destination hosts (can be used if it is an .onion server). # socks_mode: onion -# Limit number of threads a client can spawn to process proxy commands in parrallel. +# Limit number of threads a client can spawn to process proxy commands in parallel. # client_concurrency: 32 [INACTIVE_CLIENTS] @@ -823,7 +823,7 @@ Follow the steps to secure your CA keys: /etc/opt/simplex/ca.key ``` -3. Delete the CA key from the server. **Please make sure you've saved you CA key somewhere safe. Otherwise, you would lose the ability to [rotate the online certificate](#online-certificate-rotation)**: +3. Delete the CA key from the server. **Please make sure you've saved your CA key somewhere safe. Otherwise, you would lose the ability to [rotate the online certificate](#online-certificate-rotation)**: ```sh rm /etc/opt/simplex/ca.key @@ -913,9 +913,9 @@ SMP-server can also be deployed to be available via [Tor](https://www.torproject 1. Install tor: - We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [official tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. - - Configure offical Tor PPA repository: + - Configure official Tor PPA repository: ```sh CODENAME="$(lsb_release -c | awk '{print $2}')" @@ -951,12 +951,12 @@ SMP-server can also be deployed to be available via [Tor](https://www.torproject vim /etc/tor/torrc ``` - And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: these are comments about each individual option. ```sh # Enable log (otherwise, tor doesn't seem to deploy onion address) Log notice file /var/log/tor/notices.log - # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonimity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, you may want to keep standard configuration instead. + # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonymity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, you may want to keep standard configuration instead. SOCKSPort 0 HiddenServiceNonAnonymousMode 1 HiddenServiceSingleHopMode 1 @@ -974,7 +974,7 @@ SMP-server can also be deployed to be available via [Tor](https://www.torproject 3. Start tor: - Enable `systemd` service and start tor. Offical `tor` is a bit flaky on the first start and may not create onion host address, so we're restarting it just in case. + Enable `systemd` service and start tor. Official `tor` is a bit flaky on the first start and may not create onion host address, so we're restarting it just in case. ```sh systemctl enable --now tor && systemctl restart tor @@ -994,7 +994,7 @@ SMP-server versions starting from `v5.8.0-beta.0` can be configured to PROXY smp 1. Install tor as described in the [previous section](#installation-for-onion-address). -2. Execute the following command to creatae a new Tor daemon instance: +2. Execute the following command to create a new Tor daemon instance: ```sh tor-instance-create tor2 @@ -1101,7 +1101,7 @@ _Please note:_ this configuration is supported since `v6.1.0-beta.2`. hosting_country: ``` -2. Install the webserver. For easy deployment we'll describe the installtion process of [Caddy](https://caddyserver.com) webserver on Ubuntu server: +2. Install the webserver. For easy deployment we'll describe the installation process of [Caddy](https://caddyserver.com) webserver on Ubuntu server: 1. Install the packages: @@ -1127,7 +1127,7 @@ _Please note:_ this configuration is supported since `v6.1.0-beta.2`. sudo apt update && sudo apt install caddy ``` - [Full Caddy instllation instructions](https://caddyserver.com/docs/install) + [Full Caddy installation instructions](https://caddyserver.com/docs/install) 3. Replace Caddy configuration with the following: @@ -1176,12 +1176,12 @@ _Please note:_ this configuration is supported since `v6.1.0-beta.2`. key_name='web.key' cert_name='web.crt' - # Copy certifiacte from Caddy directory to smp-server directory + # Copy certificate from Caddy directory to smp-server directory cp "${folder_in}/${domain}.crt" "${folder_out}/${cert_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${cert_name}" - # Copy certifiacte key from Caddy directory to smp-server directory + # Copy certificate key from Caddy directory to smp-server directory cp "${folder_in}/${domain}.key" "${folder_out}/${key_name}" # Assign correct permissions chown "$user":"$group" "${folder_out}/${key_name}" @@ -1237,7 +1237,7 @@ smp://[:]@[,] - **optional** `` - Your configured password of `smp-server`. You can check your configured pasword in `/etc/opt/simplex/smp-server.ini`, under `[AUTH]` section in `create_password:` field. + Your configured password of `smp-server`. You can check your configured password in `/etc/opt/simplex/smp-server.ini`, under `[AUTH]` section in `create_password:` field. - ``, **optional** `` @@ -1368,9 +1368,9 @@ Here's the full list of commands, their descriptions and who can access them. | `stats-rts` | GHC/Haskell statistics. Can be enabled with `+RTS -T -RTS` option | - | | `clients` | Clients information. Useful for debugging. | yes | | `sockets` | General sockets information. | - | -| `socket-threads` | Thread infomation per socket. Useful for debugging. | yes | +| `socket-threads` | Thread information per socket. Useful for debugging. | yes | | `threads` | Threads information. Useful for debugging. | yes | -| `server-info` | Aggregated server infomation. | - | +| `server-info` | Aggregated server information. | - | | `delete` | Delete known queue. Useful for content moderation. | - | | `save` | Save queues/messages from memory. | yes | | `help` | Help menu. | - | @@ -1417,31 +1417,31 @@ fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,m | 20 | `pRelays_pRequests` | - requests | | 21 | `pRelays_pSuccesses` | - successes | | 22 | `pRelays_pErrorsConnect` | - connection errors | -| 23 | `pRelays_pErrorsCompat` | - compatability errors | +| 23 | `pRelays_pErrorsCompat` | - compatibility errors | | 24 | `pRelays_pErrorsOther` | - other errors | | Requested sessions with own relays: | | 25 | `pRelaysOwn_pRequests` | - requests | | 26 | `pRelaysOwn_pSuccesses` | - successes | | 27 | `pRelaysOwn_pErrorsConnect` | - connection errors | -| 28 | `pRelaysOwn_pErrorsCompat` | - compatability errors | +| 28 | `pRelaysOwn_pErrorsCompat` | - compatibility errors | | 29 | `pRelaysOwn_pErrorsOther` | - other errors | | Message forwards to all relays: | | 30 | `pMsgFwds_pRequests` | - requests | | 31 | `pMsgFwds_pSuccesses` | - successes | | 32 | `pMsgFwds_pErrorsConnect` | - connection errors | -| 33 | `pMsgFwds_pErrorsCompat` | - compatability errors | +| 33 | `pMsgFwds_pErrorsCompat` | - compatibility errors | | 34 | `pMsgFwds_pErrorsOther` | - other errors | | Message forward to own relays: | | 35 | `pMsgFwdsOwn_pRequests` | - requests | | 36 | `pMsgFwdsOwn_pSuccesses` | - successes | | 37 | `pMsgFwdsOwn_pErrorsConnect` | - connection errors | -| 38 | `pMsgFwdsOwn_pErrorsCompat` | - compatability errors | +| 38 | `pMsgFwdsOwn_pErrorsCompat` | - compatibility errors | | 39 | `pMsgFwdsOwn_pErrorsOther` | - other errors | | Received message forwards: | | 40 | `pMsgFwdsRecv` | | -| Message queue subscribtion errors: | +| Message queue subscription errors: | | 41 | `qSub` | All | -| 42 | `qSubAuth` | Authentication erorrs | +| 42 | `qSubAuth` | Authentication errors | | 43 | `qSubDuplicate` | Duplicate SUB errors | | 44 | `qSubProhibited` | Prohibited SUB errors | | Message errors: | @@ -1526,9 +1526,9 @@ To update your smp-server to latest version, choose your installation method and sudo systemctl start smp-server ``` - - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + - [Official installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - 1. Execute the followin command: + 1. Execute the following command: ```sh sudo simplex-servers-update @@ -1640,7 +1640,7 @@ To reproduce the build you must have: ## Configuring the app to use the server -To configure the app to use your messaging server copy it's full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them. +To configure the app to use your messaging server copy its full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them. It is also possible to share the address of your server with your friends by letting them scan QR code from server settings - it will include server password, so they will be able to receive messages via your server as well. diff --git a/docs/SIMPLEX.md b/docs/SIMPLEX.md index ec25afaf88..e24275d656 100644 --- a/docs/SIMPLEX.md +++ b/docs/SIMPLEX.md @@ -89,7 +89,7 @@ There are several P2P chat/messaging protocols and implementations that aim to s 5. All known P2P networks are likely to be vulnerable to [Sybil attack][12], because each node is discoverable, and the network operates as a whole. Known measures to reduce the probability of the Sybil attack either require a centralized component or expensive [proof of work][13]. The proposed design, on the opposite, has no server discoverability - servers are not connected, not known to each other and to all clients. The SimpleX network is fragmented and operates as multiple isolated connections. It makes network-wide attacks on SimpleX network impossible - even if some servers are compromised, other parts of the network can operate normally, and affected clients can switch to using other servers without losing contacts or messages. -6. P2P networks are likely to be [vulnerable][14] to [DRDoS attack][15]. In the proposed design clients only relay traffic from known trusted connection and cannot be used to reflect and amplify the traffic in the whole network. +6. P2P networks are likely to be [vulnerable][14] to [DRDoS attack][15]. In the proposed design clients only relay traffic from known trusted connections and cannot be used to reflect and amplify the traffic in the whole network. [1]: https://en.wikipedia.org/wiki/End-to-end_encryption [2]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md index 2b1febb6f2..a0250b6ab2 100644 --- a/docs/TRANSLATIONS.md +++ b/docs/TRANSLATIONS.md @@ -35,7 +35,7 @@ The steps are: ### Translating Android app -1. Please start from [Android app](https://hosted.weblate.org/projects/simplex-chat/android/), both when you do the most time-consuming initial translation, and add any strings later. Firstly, iOS strings can be a bit delayed from appearing in Weblate, as it requires a manual step from us before they are visible. Secondary, Android app is set up as a glossary for iOS app, and 2/3 of all strings require just to clicks to transfer them from Android to iOS (it still takes some time, Weblate doesn't automate it, unfortunately). +1. Please start from [Android app](https://hosted.weblate.org/projects/simplex-chat/android/), both when you do the most time-consuming initial translation, and add any strings later. Firstly, iOS strings can be a bit delayed from appearing in Weblate, as it requires a manual step from us before they are visible. Secondly, Android app is set up as a glossary for iOS app, and 2/3 of all strings require just two clicks to transfer them from Android to iOS (it still takes some time, Weblate doesn't automate it, unfortunately). 2. Some of the strings do not need translations, but they still need to be copied over - there is a button in weblate UI for that: diff --git a/docs/WEBRTC.md b/docs/WEBRTC.md index a48cd12b00..b4862e0d5b 100644 --- a/docs/WEBRTC.md +++ b/docs/WEBRTC.md @@ -18,7 +18,7 @@ For this guide, we'll be using the most featureful and battle-tested STUN/TURN s 1. Install `coturn` package from the main repository. ```sh -apt update && apt install coturn` +apt update && apt install coturn ``` 2. Uncomment `TURNSERVER_ENABLED=1` from `/etc/default/coturn`: @@ -44,7 +44,7 @@ user=$YOUR_LOGIN:$YOUR_PASSWORD server-name=$YOUR_DOMAIN # The default realm to be used for the users when no explicit origin/realm relationship was found realm=$YOUR_DOMAIN -# Path to your certificates. Make sure they're readable by cotun process user/group +# Path to your certificates. Make sure they're readable by coturn process user/group cert=/var/lib/turn/cert.pem pkey=/var/lib/turn/key.pem # Use 2066 bits predefined DH TLS key @@ -97,7 +97,7 @@ To configure your mobile app to use your server: 1. Open `Settings / Network & Servers / WebRTC ICE servers` and switch toggle `Configure ICE servers`. -2. Enter all server addresses in the field, one per line, for example if you servers are on the port 5349: +2. Enter all server addresses in the field, one per line, for example if your servers are on the port 5349: ``` stun:stun.example.com:5349 @@ -116,7 +116,7 @@ This is it - you now can make audio and video calls via your own server, without ping ``` - If packets being transmitted, server is up! + If packets are being transmitted, the server is up! - **Determine if ports are open**: diff --git a/docs/XFTP-SERVER.md b/docs/XFTP-SERVER.md index ba4770644e..43edbdda7f 100644 --- a/docs/XFTP-SERVER.md +++ b/docs/XFTP-SERVER.md @@ -9,7 +9,7 @@ revision: 31.07.2023 - [Overview](#overview) - [Installation options](#installation-options) - [systemd service](#systemd-service) with [installation script](#installation-script) or [manually](#manual-deployment) - - [docker container](#docker-сontainer) + - [docker container](#docker-container) - [Linode marketplace](#linode-marketplace) - [Tor installation](#tor-installation) - [Configuration](#configuration) @@ -72,7 +72,7 @@ Manual installation is the most advanced deployment that provides the most flexi 1. Install binary: - - Using offical binaries: + - Using official binaries: ```sh curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server && chmod +x /usr/local/bin/xftp-server @@ -129,9 +129,9 @@ Manual installation is the most advanced deployment that provides the most flexi And execute `sudo systemctl daemon-reload`. -### Docker сontainer +### Docker container -You can deploy smp-server using Docker Compose. This is second recommended option due to its popularity and relatively easy deployment. +You can deploy xftp-server using Docker Compose. This is the second recommended option due to its popularity and relatively easy deployment. This deployment provides two Docker Compose files: the **automatic** one and **manual**. If you're not sure, choose **automatic**. @@ -197,9 +197,9 @@ xftp-server can also be deployed to serve from [tor](https://www.torproject.org) 1. Install tor: - We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [official tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. - - Configure offical Tor PPA repository: + - Configure official Tor PPA repository: ```sh CODENAME="$(lsb_release -c | awk '{print $2}')" @@ -235,10 +235,10 @@ xftp-server can also be deployed to serve from [tor](https://www.torproject.org) vim /etc/tor/torrc ``` - And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: these are comments about each individual option. ```sh - # Enable log (otherwise, tor doesn't seemd to deploy onion address) + # Enable log (otherwise, tor doesn't seem to deploy onion address) Log notice file /var/log/tor/notices.log # Enable single hop routing (2 options below are dependencies of third). Will reduce latency in exchange of anonimity (since tor runs alongside xftp-server and onion address will be displayed in clients, this is totally fine) SOCKSPort 0 @@ -257,7 +257,7 @@ xftp-server can also be deployed to serve from [tor](https://www.torproject.org) 3. Start tor: - Enable `systemd` service and start tor. Offical `tor` is a bit flunky on the first start and may not create onion host address, so we're restarting it just in case. + Enable `systemd` service and start tor. Official `tor` is a bit flaky on the first start and may not create onion host address, so we're restarting it just in case. ```sh systemctl enable tor && systemctl start tor && systemctl restart tor @@ -356,7 +356,7 @@ To password-protect your `xftp-server`, change it in the configuration: ``` --- -After that, your installation is complete and you should see in your teminal output something like this: +After that, your installation is complete and you should see in your terminal output something like this: ```sh Certificate request self-signature ok @@ -398,7 +398,7 @@ xftp://[:]@[,] - **optional** `` - Your configured password of `xftp-server`. You can check your configured pasword in `/etc/opt/simplex-xftp/file-server.ini`, under `[AUTH]` section in `create_password:` field. + Your configured password of `xftp-server`. You can check your configured password in `/etc/opt/simplex-xftp/file-server.ini`, under `[AUTH]` section in `create_password:` field. - ``, **optional** `` @@ -609,8 +609,8 @@ To update your XFTP server to latest version, choose your installation method an sudo systemctl start xftp-server ``` - - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - 1. Execute the followin command: + - [Official installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + 1. Execute the following command: ```sh sudo simplex-servers-update ``` diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index d5c3046e3a..36692739dd 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -351,7 +351,7 @@ export interface APINewPublicGroup { } export namespace APINewPublicGroup { - export type Response = CR.PublicGroupCreated | CR.ChatCmdError + export type Response = CR.PublicGroupCreated | CR.PublicGroupCreationFailed | CR.ChatCmdError export function cmdString(self: APINewPublicGroup): string { return '/_public group ' + self.userId + (self.incognito ? ' incognito=on' : '') + ' ' + self.relayIds.join(',') + ' ' + JSON.stringify(self.groupProfile) @@ -471,6 +471,7 @@ export namespace APIAddContact { export interface APIConnectPlan { userId: number // int64 connectionLink?: string + linkOwnerSig?: T.LinkOwnerSig } export namespace APIConnectPlan { diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index 8d4f68c000..02aa29444b 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -28,6 +28,7 @@ export type ChatResponse = | CR.GroupLinkDeleted | CR.GroupCreated | CR.PublicGroupCreated + | CR.PublicGroupCreationFailed | CR.GroupRelays | CR.GroupMembers | CR.GroupUpdated @@ -81,6 +82,7 @@ export namespace CR { | "groupLinkDeleted" | "groupCreated" | "publicGroupCreated" + | "publicGroupCreationFailed" | "groupRelays" | "groupMembers" | "groupUpdated" @@ -258,6 +260,12 @@ export namespace CR { groupRelays: T.GroupRelay[] } + export interface PublicGroupCreationFailed extends Interface { + type: "publicGroupCreationFailed" + user: T.User + addRelayResults: T.AddRelayResult[] + } + export interface GroupRelays extends Interface { type: "groupRelays" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 621a69dcc8..6f4f0b6525 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -17,6 +17,11 @@ export interface AChatItem { chatItem: ChatItem } +export interface AddRelayResult { + relay: UserChatRelay + relayError?: ChatError +} + export interface AddressSettings { businessAddress: boolean autoAccept?: AutoAccept @@ -1986,6 +1991,7 @@ export namespace ContactAddressPlan { export interface Ok extends Interface { type: "ok" contactSLinkData_?: ContactShortLinkData + ownerVerification?: OwnerVerification } export interface OwnLink extends Interface { @@ -2565,9 +2571,16 @@ export type GroupLinkPlan = | GroupLinkPlan.ConnectingConfirmReconnect | GroupLinkPlan.ConnectingProhibit | GroupLinkPlan.Known + | GroupLinkPlan.NoRelays export namespace GroupLinkPlan { - export type Tag = "ok" | "ownLink" | "connectingConfirmReconnect" | "connectingProhibit" | "known" + export type Tag = + | "ok" + | "ownLink" + | "connectingConfirmReconnect" + | "connectingProhibit" + | "known" + | "noRelays" interface Interface { type: Tag @@ -2577,6 +2590,7 @@ export namespace GroupLinkPlan { type: "ok" groupSLinkInfo_?: GroupShortLinkInfo groupSLinkData_?: GroupShortLinkData + ownerVerification?: OwnerVerification } export interface OwnLink extends Interface { @@ -2597,6 +2611,11 @@ export namespace GroupLinkPlan { type: "known" groupInfo: GroupInfo } + + export interface NoRelays extends Interface { + type: "noRelays" + groupSLinkData_?: GroupShortLinkData + } } export interface GroupMember { @@ -2787,6 +2806,7 @@ export namespace InvitationLinkPlan { export interface Ok extends Interface { type: "ok" contactSLinkData_?: ContactShortLinkData + ownerVerification?: OwnerVerification } export interface OwnLink extends Interface { @@ -2856,6 +2876,12 @@ export namespace LinkContent { } } +export interface LinkOwnerSig { + ownerId?: string + chatBinding: string + ownerSig: string +} + export interface LinkPreview { uri: string title: string @@ -2973,6 +2999,7 @@ export namespace MsgContent { type: "chat" text: string chatLink: MsgChatLink + ownerSig?: LinkOwnerSig } export interface Unknown extends Interface { @@ -3131,6 +3158,25 @@ export interface NoteFolder { unread: boolean } +export type OwnerVerification = OwnerVerification.Verified | OwnerVerification.Failed + +export namespace OwnerVerification { + export type Tag = "verified" | "failed" + + interface Interface { + type: Tag + } + + export interface Verified extends Interface { + type: "verified" + } + + export interface Failed extends Interface { + type: "failed" + reason: string + } +} + export interface PendingContactConnection { pccConnId: number // int64 pccAgentConnId: string @@ -3650,6 +3696,7 @@ export enum RelayStatus { Invited = "invited", Accepted = "accepted", Active = "active", + Inactive = "inactive", } export enum ReportReason { diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 657c6e05f2..21e00e7399 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "simplex-chat", - "version": "6.5.0-beta.4.4", + "version": "6.5.0-beta.4.5", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js index 6b8f583155..25d4127ea4 100644 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -4,7 +4,7 @@ const path = require('path'); const extract = require('extract-zip'); const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; -const RELEASE_TAG = 'v6.5.0-beta.4'; +const RELEASE_TAG = 'v6.5.0-beta.9'; const ROOT_DIR = process.cwd(); // Root of the package being installed const LIBS_DIR = path.join(ROOT_DIR, 'libs') const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt'); diff --git a/plans/2026-04-06-onboarding-cards-compose.md b/plans/2026-04-06-onboarding-cards-compose.md new file mode 100644 index 0000000000..00df159eee --- /dev/null +++ b/plans/2026-04-06-onboarding-cards-compose.md @@ -0,0 +1,492 @@ +# Onboarding Cards — Compose (Android/Desktop) Implementation Plan + +References the layout specification in `plans/2026-04-06-onboarding-cards-ios.md`. + +## Scope + +Same as iOS: Screens 1 and 2 with paging transition. Modal sheets for deeper views. No banner, no standalone onboarding variants. + +## New file + +`common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt` + +## Assets + +8 card stub SVGs needed in `assets/default/MR/images/` (same names as the real PNGs, with `.svg` extension): +- `card_let_someone_connect_to_you_alpha.svg` / `_light.svg` +- `card_connect_via_link_alpha.svg` / `_light.svg` +- `card_invite_someone_privately_alpha.svg` / `_light.svg` +- `card_create_your_public_address_alpha.svg` / `_light.svg` + +Real PNGs already generated in art repo `multiplatform/resources/MR/images/`. + +## Onboarding condition (shared by Android and Desktop) + +Placed in `ConnectOnboardingView.kt` as top-level functions, accessible from both `ChatListView.kt` and `App.kt`: + +```kotlin +@Composable +fun shouldShowOnboarding(): Boolean { + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val chats = chatModel.chats.value + return !addressCreationCardShown.value && chats.isNotEmpty() && noConversationChatsYet(chats) +} + +fun noConversationChatsYet(chats: List): Boolean = + chats.all { chat -> + when (val c = chat.chatInfo) { + is ChatInfo.Local -> true + is ChatInfo.Direct -> c.contact.chatDeleted || c.contact.isContactCard + is ChatInfo.Group -> false + is ChatInfo.ContactRequest -> true + is ChatInfo.ContactConnection -> true + is ChatInfo.InvalidJSON -> true + } + } +``` + +`shouldShowOnboarding` is `@Composable` (reads reactive state) and public — called from both `ChatListView.kt` and `App.kt`. `noConversationChatsYet` is a pure function, also public (used by auto-dismiss LaunchedEffect). + +### Auto-dismiss + +```kotlin +LaunchedEffect(chatModel.chats.value.size) { + if (!noConversationChatsYet(chatModel.chats.value)) { + appPrefs.addressCreationCardShown.set(true) + } +} +``` + +Placed in `ChatListWithLoadingScreen`. + +## Android integration + +### In `ChatListView.kt` — `ChatListWithLoadingScreen` (line 291) + +Change from: +```kotlin +private fun BoxScope.ChatListWithLoadingScreen(searchText, listState) { + if (!chatModel.desktopNoUserNoRemote) { ChatList(...) } + if (chatModel.chats.value.isEmpty() && ...) { Text("Loading/empty") } +} +``` + +To: +```kotlin +private fun BoxScope.ChatListWithLoadingScreen(searchText, listState) { + val chats = chatModel.chats.value + when { + chats.isEmpty() && !chatModel.switchingUsersAndHosts.value + && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == null -> { + Text(stringResource(MR.strings.loading_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } + shouldShowOnboarding() -> { + if (appPlatform.isAndroid) { + ConnectOnboardingView() + } + // Desktop: empty — overlay in DesktopScreen handles it + } + !chatModel.desktopNoUserNoRemote -> { + ChatList(searchText = searchText, listState) + } + } + // Auto-dismiss + LaunchedEffect(chats.size) { + if (chats.isNotEmpty() && !noConversationChatsYet(chats)) { + appPrefs.addressCreationCardShown.set(true) + } + } +} +``` + +Toolbar is a sibling in the parent `Box` (lines 150-174), stays visible. + +## Desktop integration + +### Architecture + +The overlay is the PRIMARY UI surface during onboarding. ALL interaction happens inside it — card taps, toolbar button modals, everything. `ModalManager.start` renders INTO the overlay instead of into the start panel. + +### Overlay structure in `DesktopScreen` (App.kt) + +Two visual layers in the overlay, both full-width: + +1. **Background layer:** covers center+end area only (padded left by start panel width). Opaque `MaterialTheme.colors.background`. Hides center panel content ("No selected chat") while leaving start panel fully visible underneath. + +2. **Content layer:** full window width, no background. Cards render here, centered in the full window. Clicks outside cards fall through to the start panel below. + +Both layers have top/bottom padding for toolbar height (`AppBarHeight * fontSizeSqrtMultiplier`). + +```kotlin +if (shouldShowOnboarding()) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val toolbarPadding = AppBarHeight * fontSizeSqrtMultiplier + val topPad = if (!oneHandUI.value) toolbarPadding else 0.dp + val bottomPad = if (oneHandUI.value) toolbarPadding else 0.dp + + // Background — center+end only + Box( + Modifier + .fillMaxSize() + .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, top = topPad, bottom = bottomPad) + .background(MaterialTheme.colors.background) + ) + + // Content — full width, cards centered + Box( + Modifier + .fillMaxSize() + .padding(top = topPad, bottom = bottomPad), + contentAlignment = Alignment.Center + ) { + ConnectOnboardingView() + } +} +``` + +Z-order: above panels and vertical divider, below `ModalManager.fullscreen`. + +### Start panel modal redirection + +During onboarding, `ModalManager.start.showInView()` renders INSIDE the overlay instead of in the start panel Box. + +In `DesktopScreen`: +```kotlin +// Start panel modals — normal location +Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { + if (!shouldShowOnboarding()) { + ModalManager.start.showInView() + } + SwitchingUsersView() +} +``` + +Inside `ConnectOnboardingView`, on desktop: +- Watch `ModalManager.start.hasModalsOpen` +- When a start modal opens (from toolbar + button, avatar, or card tap): + 1. Cards shift RIGHT and fade to ~30% opacity (animated) + 2. `ModalManager.start.showInView()` renders on the LEFT side of the overlay with left-to-right slide animation + 3. This is the FIRST modal opening — it slides left-to-right + 4. Subsequent modals within the start modal stack open right-to-left as usual (standard `ModalManager` behavior inside the rendered area) +- When all start modals close: reverse animation — modal area slides left, cards restore position and opacity +- Clicking a faded card triggers `ModalManager.start.closeModals()` to dismiss and restore cards. This requires swapping card `onClick` handlers when `startModalsOpen` is true — each card's onClick becomes `{ ModalManager.start.closeModals() }` instead of its normal action. + +```kotlin +// Inside ConnectOnboardingView, desktop only: +val startModalsOpen = ModalManager.start.hasModalsOpen +val cardOffset by animateFloatAsState(if (startModalsOpen) 0.3f else 0f) +val cardAlpha by animateFloatAsState(if (startModalsOpen) 0.3f else 1f) +val modalSlide by animateFloatAsState(if (startModalsOpen) 0f else -1f) + +Box(Modifier.fillMaxSize()) { + // Modal area — slides from left + if (appPlatform.isDesktop) { + Box( + Modifier + .fillMaxHeight() + .widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) + .graphicsLayer { translationX = modalSlide * size.width } + ) { + ModalManager.start.showInView() + } + } + + // Cards — shift right and fade when modal open + Box( + Modifier + .fillMaxSize() + .graphicsLayer { + if (appPlatform.isDesktop) { + translationX = cardOffset * size.width + alpha = cardAlpha + } + } + ) { + HorizontalPager(...) { /* pages */ } + } +} +``` + +Card taps use `ModalManager.start.showModalCloseable` on ALL platforms — same code. On Android, the modal renders in the normal start panel modal area. On desktop during onboarding, the modal renders inside the overlay via the redirected `showInView()`. + +### Card tap actions — same on all platforms + +```kotlin +val openConnectViaLink = { + ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, ..., close = close) + } +} +``` + +No platform branching needed. `ModalManager.start` handles the modal lifecycle. Only the rendering location changes. + +### Suppress "No selected chat" in `CenterPartOfScreen` (line 373) + +```kotlin +null -> { + if (!shouldShowOnboarding() && !rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { + Box(...) { Text(stringResource(...)) } + } else if (!shouldShowOnboarding()) { + ModalManager.center.showInView() + } +} +``` + +When onboarding active: center panel shows nothing (overlay covers it visually). + +### Desktop HorizontalPager: tap only + +`userScrollEnabled = !appPlatform.isDesktop` — disables mouse swipe on desktop. + +## Revision 2 — Bug fixes from initial implementation + +### Fix 1: `fillMaxSize()` overrides `widthIn(max:)` + +In both page composables, the Column has `Modifier.fillMaxSize().widthIn(max = 500.dp)`. `fillMaxSize()` sets width to maximum, overriding the `widthIn` constraint. + +Fix: `Modifier.fillMaxHeight().widthIn(max = 500.dp)` — only fill height, let widthIn cap the width. + +### Fix 2: Modifier order — background before padding + +In the overlay Box: `.fillMaxSize().background(color).padding(...)` paints background over toolbar area. + +Fix: `.fillMaxSize().padding(...).background(color)` — padding first, background only fills content area. + +Superseded by the two-layer approach above — background layer is separate from content layer. + +### Fix 3: "You have no chats" text dropped + +The `when` block in `ChatListWithLoadingScreen` replaced two independent `if` blocks with mutually exclusive branches, dropping the "You have no chats" case. + +Fix: revert to the original `if` block structure, adding onboarding as the first check: +```kotlin +private fun BoxScope.ChatListWithLoadingScreen(searchText, listState) { + if (shouldShowOnboarding()) { + if (appPlatform.isAndroid) { + ConnectOnboardingView() + } + } else { + if (!chatModel.desktopNoUserNoRemote) { + ChatList(searchText = searchText, listState) + } + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + Text(stringResource( + if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats + ), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } + } + // Auto-dismiss + LaunchedEffect(chatModel.chats.value.size) { ... } +} +``` + +This preserves the original loading/empty behavior exactly. The onboarding branch is checked first — when active, it replaces everything. When inactive, original code runs unchanged. + +## ConnectOnboardingView composable + +### Structure + +```kotlin +@Composable +fun ConnectOnboardingView() { + val pagerState = rememberPagerState(initialPage = 0) { 2 } + val scope = rememberCoroutineScope() + + HorizontalPager(state = pagerState, userScrollEnabled = true) { page -> + when (page) { + 0 -> TalkToSomeonePage( + onLetSomeoneConnect = { scope.launch { pagerState.animateScrollToPage(1) } }, + onConnectViaLink = { ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = close) + }} + ) + 1 -> ConnectWithSomeonePage( + onBack = { scope.launch { pagerState.animateScrollToPage(0) } }, + onInviteSomeone = { ModalManager.start.showModalCloseable { close -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) + }}, + onCreateAddress = { ModalManager.start.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) + }} + ) + } + } +} +``` + +### Page layout + +Each page uses `BoxWithConstraints` to compute card dimensions: + +```kotlin +@Composable +private fun TalkToSomeonePage(onLetSomeoneConnect: () -> Unit, onConnectViaLink: () -> Unit) { + BoxWithConstraints(Modifier.fillMaxSize()) { + val isLandscape = maxWidth > maxHeight + val padding = 16.dp + val spacing = 16.dp + val cardWidth = if (isLandscape) (maxWidth - padding * 2 - spacing) / 2 else maxWidth - padding * 2 + val maxCardHeight = cardWidth * 0.75f + + Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { + pageHeader("Talk to someone", showBack = false, isLandscape = isLandscape) + Spacer(Modifier.weight(1f).defaultMinSize(minHeight = 16.dp)) + cardPair(isLandscape, padding, spacing, maxCardHeight) { + // card1 and card2 + } + Spacer(Modifier.weight(1f).defaultMinSize(minHeight = 16.dp)) + } + } +} +``` + +### pageHeader composable + +Shared by both pages. No duplication: + +```kotlin +@Composable +private fun pageHeader(title: String, showBack: Boolean, isLandscape: Boolean, onBack: (() -> Unit)? = null) { + val titleView = @Composable { + Text( + stringResource(title), + style = MaterialTheme.typography.h1, // largeTitle equivalent + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + if (isLandscape) { + Box(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + if (showBack && onBack != null) { + backButton(onBack, Modifier.align(Alignment.CenterStart)) + } + titleView() + } + } else { + Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + if (showBack && onBack != null) { + backButton(onBack, Modifier.align(Alignment.Start)) + } else { + Spacer(Modifier.height(AppBarHeight)) + } + titleView() + } + } +} +``` + +Back button spacer uses `AppBarHeight` (56.dp) to match the platform's back button area, not iOS's 44pt. + +### cardPair composable + +Shared layout helper, no card duplication: + +```kotlin +@Composable +private fun cardPair( + isLandscape: Boolean, + padding: Dp, + spacing: Dp, + maxCardHeight: Dp, + card1: @Composable () -> Unit, + card2: @Composable () -> Unit +) { + if (isLandscape) { + Row(Modifier.padding(horizontal = padding), horizontalArrangement = Arrangement.spacedBy(spacing)) { + Box(Modifier.weight(1f).heightIn(max = maxCardHeight)) { card1() } + Box(Modifier.weight(1f).heightIn(max = maxCardHeight)) { card2() } + } + } else { + Column(Modifier.padding(horizontal = padding), verticalArrangement = Arrangement.spacedBy(spacing)) { + Box(Modifier.fillMaxWidth().heightIn(max = maxCardHeight)) { card1() } + Box(Modifier.fillMaxWidth().heightIn(max = maxCardHeight)) { card2() } + } + } +} +``` + +### OnboardingCardView composable + +```kotlin +@Composable +fun OnboardingCardView( + imageName: ImageResource, + imageNameLight: ImageResource, + icon: ImageResource, + title: String, + subtitle: String? = null, + labelHeightRatio: Float, + onClick: () -> Unit +) +``` + +Key Compose details (from layout-compose.md checklist): +- **Image:** `contentScale = ContentScale.Fit`, `Modifier.fillMaxSize()` — scaled AND centered ✓ +- **Gradient:** `Brush.linearGradient(colorStops, start, end)` with pixel Offsets computed from image area measured size via `Modifier.onSizeChanged` or `BoxWithConstraints` +- **Gradient math:** identical to iOS — same function ported to Kotlin, same angle/scale/aspect-ratio correction +- **Corner radius:** `RoundedCornerShape(24.dp)` with `Modifier.clip()` +- **Dark/light:** `if (isInDarkTheme()) imageNameLight else imageName` for image, gradient stops selected by theme +- **Conditional assets:** `if (BuildConfigCommon.SIMPLEX_ASSETS) { Image(...) }` +- **Clickable:** `Modifier.clip(RoundedCornerShape(24.dp)).clickable(onClick = onClick)` — clip first so ripple is bounded + +#### Label stripe background + +Use the same pattern as the toolbar (from DefaultTopAppBar.kt line 43-65): +```kotlin +MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + .copy(alpha = appPrefs.inAppBarsAlpha.get()) +``` + +This exactly matches the toolbar appearance, including the user's bar transparency preference. + +#### Gradient in Compose + +```kotlin +// Compute inside BoxWithConstraints or onSizeChanged callback +val imageAreaSize = Size(width, imageHeight) +val (startUnit, endUnit) = gradientPoints( + aspectRatio = imageAreaSize.height / imageAreaSize.width, + scale = if (isInDarkTheme()) 1.5f else 1.2f +) +val brush = Brush.linearGradient( + colorStops = if (isInDarkTheme()) darkStops else lightStops, + start = Offset(startUnit.x * imageAreaSize.width, startUnit.y * imageAreaSize.height), + end = Offset(endUnit.x * imageAreaSize.width, endUnit.y * imageAreaSize.height) +) +``` + +### Card icons (Moko resource names) + +Screen 1: +- "Let someone connect to you" — `MR.images.ic_add_link` +- "Connect via link or QR code" — `MR.images.ic_qr_code` + +Screen 2: +- "Invite someone privately" — `MR.images.ic_add_link` +- "Create your public address" — `MR.images.ic_qr_code` + +### Strings + +8 new entries in `strings.xml` (`MR/base/strings.xml`): +```xml +Talk to someone +Let someone connect to you +Connect via link or QR code +Connect with someone +Invite someone privately +A link for one person to connect +Create your public address +For anyone to reach you +``` + +## Files changed + +- `ChatListView.kt` — add `shouldShowOnboarding`, `noConversationChatsYet`, modify `ChatListWithLoadingScreen`, add auto-dismiss `LaunchedEffect` +- `App.kt` — add desktop overlay in `DesktopScreen`, suppress "No selected chat" in `CenterPartOfScreen` +- `MR/base/strings.xml` — 8 new strings +- **New:** `OnboardingCards.kt` — `ConnectOnboardingView`, `OnboardingCardView`, `TalkToSomeonePage`, `ConnectWithSomeonePage`, `pageHeader`, `cardPair`, `shouldShowOnboarding`, `noConversationChatsYet`, gradient math +- **New:** 8 stub SVGs in `assets/default/MR/images/` diff --git a/plans/2026-04-06-onboarding-cards-ios.md b/plans/2026-04-06-onboarding-cards-ios.md new file mode 100644 index 0000000000..f20936ea05 --- /dev/null +++ b/plans/2026-04-06-onboarding-cards-ios.md @@ -0,0 +1,501 @@ +# Onboarding Cards — Layout Specification & iOS Implementation Plan + +## Layout Specification (cross-platform) + +This section is the authoritative reference for implementing on any platform. + +### Overall structure + +Two screens, each with a title and two tappable cards. Screens are connected by a horizontal paging transition (swipe or tap). Screen 1 has no back button; Screen 2 has a back button. Deeper views (1-time link, connect via link, SimpleX address) open as modal sheets from card taps, NOT as navigation pushes. + +The chat list toolbar (top or bottom depending on platform/settings) remains visible on both screens — the onboarding content occupies only the chat list content area. + +### Page header + +Each page has a header area containing: +- **Back button area:** fixed height 44pt. Screen 1: empty space. Screen 2: "< Back" button left-aligned. +- **Title:** centered, largeTitle font, bold, single line, shrinks to 75% minimum scale factor. +- Screen 1 title: "Talk to someone" +- Screen 2 title: "Connect with someone" + +**Portrait:** back button area and title are two separate rows (VStack). +**Landscape:** back button and title share one row (ZStack — back button leading, title centered). No separate back button row — saves vertical space. + +Padding: 16pt horizontal on the header container. Back button has no padding of its own. + +### Card layout + +**Portrait:** two cards stacked vertically (VStack, spacing 16pt). +**Landscape:** two cards side-by-side (HStack, spacing 16pt). + +Card horizontal padding: 16pt each side. + +Cards are vertically centered in the remaining space below the header. Equal space above and below the card group (Spacer with minLength 16pt on both sides). + +### Card max height + +Max total card height = card width × 0.75. + +In portrait: card width = screen width − 32pt (16pt padding each side). +In landscape: card width = (screen width − 32pt − 16pt spacing) / 2. + +Card height can be less than max on small screens. Height never exceeds max. + +### Card component + +Each card is a rounded rectangle (corner radius 24pt) containing: + +1. **Image area** (top) — gradient background + alpha-channel illustration overlay +2. **Label stripe** (bottom) — toolbar material background, fixed proportional height + +#### Image area + +- Gradient fills only the image area, NOT the label stripe +- Illustration: `.resizable().scaledToFit()`, fills available space, clipped to image area + +#### Gradient + +Stops (light mode): +- `#d2e8ff` (rgb 0.824, 0.910, 1.0) at 0% +- `#cce9ff` (rgb 0.800, 0.914, 1.0) at 50% +- `#dfffff` (rgb 0.875, 1.0, 1.0) at 90% +- `#fffcea` (rgb 1.0, 0.988, 0.918) at 100% + +Stops (dark mode): +- `#040a24` (rgb 0.016, 0.039, 0.141) at 40% +- `#3854ab` (rgb 0.220, 0.329, 0.671) at 72% +- `#a8edf3` (rgb 0.659, 0.929, 0.953) at 90% +- `#fff6e0` (rgb 1.0, 0.965, 0.878) at 100% + +Angle: 80° counter-clockwise from horizontal (almost vertical, slight rightward lean at top). + +Gradient scale (center-anchored): 1.2× in light mode, 1.5× in dark mode. This pushes start/end points further from center, reducing colored corner area. + +**Gradient endpoint calculation** (accounts for variable card aspect ratio): + +The gradient must maintain a constant 80° visual angle regardless of card proportions. Given the IMAGE AREA aspect ratio `r = imageHeight / width`: + +``` +θ = 80° (in radians: 80 × π / 180) +dx = cos(θ) +dy = −sin(θ) / r + +Project four corners (±0.5, ±0.5) onto direction (dx, dy): + projections = [−0.5·dx + (−0.5)·dy, 0.5·dx + (−0.5)·dy, −0.5·dx + 0.5·dy, 0.5·dx + 0.5·dy] + tMin = min(projections), tMax = max(projections) + dLenSq = dx² + dy² + +Base endpoints: + startX = 0.5 + tMin·dx/dLenSq, startY = 0.5 + tMin·dy/dLenSq + endX = 0.5 + tMax·dx/dLenSq, endY = 0.5 + tMax·dy/dLenSq + +Apply scale S (1.2 or 1.5) center-anchored: + finalStart = (0.5 + (startX−0.5)·S, 0.5 + (startY−0.5)·S) + finalEnd = (0.5 + (endX−0.5)·S, 0.5 + (endY−0.5)·S) +``` + +Important: aspect ratio uses IMAGE AREA height (card height minus label stripe), not total card height. + +#### Label stripe + +Height relative to card width: +- Single-line labels (Screen 1): 0.132 × card width +- Two-line labels (Screen 2): 0.195 × card width + +Background: platform toolbar material (matches the app toolbar appearance). On iOS: `Material` from `ToolbarMaterial` user setting. On Android: equivalent translucent material. + +Content layout: centered horizontally. +- Icon: 24pt, theme primary/accent color +- Title: body font (17pt), medium weight, theme foreground color, single line, shrinks to 75% +- Subtitle (Screen 2 only): footnote (13pt), theme foreground at 70% opacity + +Label stripe sits below the image area — gradient does NOT extend under it. + +### Card images + +8 alpha-channel PNGs (4 illustrations × light/dark variants). + +Screen 1: +- `card-let-someone-connect-to-you-alpha` / `-light` +- `card-connect-via-link-alpha` / `-light` + +Screen 2: +- `card-invite-someone-privately-alpha` / `-light` +- `card-create-your-public-address-alpha` / `-light` + +Light/dark selection: use base name on light backgrounds, `-light` suffix on dark backgrounds. + +Gated behind build flag (`#if SIMPLEX_ASSETS` on iOS, `BuildConfigCommon.SIMPLEX_ASSETS` on Android). Without assets: gradient-only cards with label stripe, still functional. + +### Card icons (SF Symbols / Material equivalents) + +Screen 1: +- "Let someone connect to you" — `link.badge.plus` +- "Connect via link or QR code" — `qrcode.viewfinder` + +Screen 2: +- "Invite someone privately" — `link.badge.plus` +- "Create your public address" — `qrcode` + +### Card actions + +Screen 1: +- Left card ("Let someone connect to you") → paging transition to Screen 2 +- Right card ("Connect via link or QR code") → modal sheet with ConnectView + +Screen 2: +- Left card ("Invite someone privately") → modal sheet with InviteView (1-time link) +- Right card ("Create your public address") → modal sheet with UserAddressView (auto-create) + +### Onboarding visibility + +Controlled by existing user default `addressCreationCardShown` (key: `"AddressCreationCardShown"`). + +Show onboarding when: +- `addressCreationCardShown == false` +- Chat list is not empty (chats have loaded) +- All chats are "ignorable" (note folders, deleted contacts, contact cards, pending connections/requests, invalid JSON) +- Any group = real conversation → onboarding hidden + +Auto-dismiss: when first real conversation appears, set `addressCreationCardShown = true` permanently. Observed via chat list count changes. + +### Strings (8) + +- "Talk to someone" +- "Let someone connect to you" +- "Connect via link or QR code" +- "Connect with someone" +- "Invite someone privately" +- "A link for one person to connect" +- "Create your public address" +- "For anyone to reach you" + +--- + +## Scope + +Screens 1 and 2 only — two card selection screens with slide navigation between them. No standalone onboarding variants of existing views. No banner. Those are separate future work. + +## New file + +`Shared/Views/NewChat/OnboardingCards.swift` — all new code in one file. + +## What it contains + +### `OnboardingCardView` — reusable card component + +```swift +struct OnboardingCardView: View { + @Environment(\.colorScheme) var colorScheme + let imageName: String // base asset name (without -light suffix) + let icon: String // SF Symbol name + let title: LocalizedStringKey + let subtitle: LocalizedStringKey? // nil for screen 1 cards + let action: () -> Void +} +``` + +Image selection follows the project convention: +- `colorScheme == .light` → `imageName` (base name, dark-colored image for light backgrounds) +- `colorScheme == .dark` → `"\(imageName)-light"` (light-colored image for dark backgrounds) + +Note: this only works when the base name does NOT already contain `-light`. The card image base names are like `card-let-someone-connect-to-you-alpha` — the `-alpha` suffix distinguishes them, and appending `-light` gives `card-let-someone-connect-to-you-alpha-light`. Correct. + +Structure (inside → out): +1. `Button(action:)` wrapping the entire card for tap handling, with `.buttonStyle(.plain)` to prevent default blue tint +2. Clipped to `RoundedRectangle(cornerRadius: 18)` +3. Inside, `ZStack(alignment: .bottom)`: + - `LinearGradient` filling the card shape + - `VStack(spacing: 0)`: + - `#if SIMPLEX_ASSETS` block: `Image` with `.resizable().scaledToFit().frame(maxWidth: .infinity, maxHeight: .infinity)` — takes all space above label. Image uses `.clipped()` to prevent overflow into label area. + - `#else` block: `Spacer()` — gradient-only card, label still functional. + - Label area with fixed height: `HStack(spacing: 8)` with `Image(systemName: icon)` (20pt) + `VStack(alignment: .leading, spacing: 2)` containing title + optional subtitle. Padded `(.horizontal, 16)` and `(.vertical, 12)`. + +Gradient stops (using `Color(red:green:blue:)` with values 0-1, no hex extension exists in the project): +- Light: + - `Color(red: 0.824, green: 0.910, blue: 1.0)` at 0.0 (#d2e8ff) + - `Color(red: 0.800, green: 0.914, blue: 1.0)` at 0.5 (#cce9ff) + - `Color(red: 0.875, green: 1.0, blue: 1.0)` at 0.9 (#dfffff) + - `Color(red: 1.0, green: 0.988, blue: 0.918)` at 1.0 (#fffcea) +- Dark: + - `Color(red: 0.016, green: 0.039, blue: 0.141)` at 0.4 (#040a24) + - `Color(red: 0.220, green: 0.329, blue: 0.671)` at 0.72 (#3854ab) + - `Color(red: 0.659, green: 0.929, blue: 0.953)` at 0.9 (#a8edf3) + - `Color(red: 1.0, green: 0.965, blue: 0.878)` at 1.0 (#fff6e0) +- Angle: 80° from vertical = 10° from horizontal. `LinearGradient(stops:..., startPoint: .init(x: 0.0, y: 0.6), endPoint: .init(x: 1.0, y: 0.4))`. Must verify visually — the exact start/end points for 80° depend on the view's aspect ratio. May need adjustment. + +Define the gradient stops as static properties on `OnboardingCardView` to avoid recomputing them on every recomposition. + +Label text styles: +- Title: `.body` weight `.semibold`, color `Color.white` in dark mode, `Color.primary` in light mode (from design: dark text on light gradient, light text on dark gradient). +- Subtitle: `.footnote`, color `.secondary` (adapts to theme). +- Icon: same color as title. + +### `TalkToSomeoneView` — Screen 1 + +```swift +struct TalkToSomeoneView: View { + @EnvironmentObject var theme: AppTheme + @State private var showConnectWithSomeone = false + @State private var showConnectViaLink = false +``` + +Body — NOT scrollable, fills available space: + +```swift +var body: some View { + VStack(spacing: 16) { + Text("Talk to someone") + .font(.largeTitle) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + + OnboardingCardView( + imageName: "card-let-someone-connect-to-you-alpha", + icon: "link", + title: "Let someone connect to you", + subtitle: nil, + action: { showConnectWithSomeone = true } + ) + .frame(maxHeight: .infinity) + .padding(.horizontal, 16) + + OnboardingCardView( + imageName: "card-connect-via-link-alpha", + icon: "qrcode", + title: "Connect via link or QR code", + subtitle: nil, + action: { showConnectViaLink = true } + ) + .frame(maxHeight: .infinity) + .padding(.horizontal, 16) + } + .padding(.vertical, 16) + .background( + NavigationLink(isActive: $showConnectWithSomeone) { + ConnectWithSomeoneView() + } label: { EmptyView() } + ) + .background( + NavigationLink(isActive: $showConnectViaLink) { + NewChatView(selection: .connect, showQRCodeScanner: true) + .navigationBarTitleDisplayMode(.inline) + } label: { EmptyView() } + ) +} +``` + +Key layout decisions: +- `.frame(maxHeight: .infinity)` on each card makes them share remaining vertical space equally after the title takes its natural height. +- `.padding(.vertical, 16)` on the VStack adds 16pt above the title and 16pt below the second card (VStack `spacing` only applies between children, not before first or after last). +- Hidden `NavigationLink(isActive:)` in `.background()` — drives navigation without affecting layout. This is the deprecated iOS 15 API but it works on iOS 16+ inside `NavigationStack` and is used throughout the existing codebase (e.g., `NewChatMenuButton.swift` lines 100-110). + +**`oneHandUI` inversion handling:** `TalkToSomeoneView` replaces `chatList` content. `chatListView` applies `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)` to the root page. The onboarding view gets inverted. It must counter-invert with `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)`. This is applied in `ChatListView.chatList`, NOT inside `TalkToSomeoneView` — the caller is responsible. When NavigationLink pushes Screen 2 or further views, those are new navigation pages outside the root page's scale effect, so they render normally. + +### `ConnectWithSomeoneView` — Screen 2 + +```swift +struct ConnectWithSomeoneView: View { + @EnvironmentObject var theme: AppTheme + @State private var showInviteSomeone = false + @State private var showCreateAddress = false +``` + +Same VStack layout as Screen 1, with these differences: +- Title: "Connect with someone" +- Card 1: imageName `"card-invite-someone-privately-alpha"`, icon `"link"`, title "Invite someone privately", subtitle "A link for one person to connect" → sets `showInviteSomeone = true` +- Card 2: imageName `"card-create-your-public-address-alpha"`, icon `"qrcode"`, title "Create your public address", subtitle "For anyone to reach you" → sets `showCreateAddress = true` + +Navigation destinations (existing views, unmodified — onboarding variants are future work): +- `showInviteSomeone` → `NewChatView(selection: .invite)` — tabbed view, 1-time link tab pre-selected. Has tabs (not ideal) but functional. +- `showCreateAddress` → `UserAddressView(shareViaProfile: false, autoCreate: true)` — auto-creates address on appear. + +Both wrapped with `.navigationBarTitleDisplayMode(.inline)`. + +Navigation bar back button shows automatically (pushed via NavigationLink within the stack). + +## Integration into ChatListView + +### In `chatList` property (line 351 of ChatListView.swift) + +Current code: +```swift +private var chatList: some View { + let cs = filteredChats() + return ZStack { + ScrollViewReader { scrollProxy in + List { ... } + } + } +} +``` + +Changed to: +```swift +@ViewBuilder +private var chatList: some View { + if shouldShowOnboarding { + TalkToSomeoneView() + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + } else { + let cs = filteredChats() + ZStack { + ScrollViewReader { scrollProxy in + List { ... } + } + } + } +} +``` + +Requires `@ViewBuilder` because `if/else` returns different view types. + +**`oneHandUI` inversion:** The `.scaleEffect(y: -1)` is applied by `chatListView` to the root page of the navigation stack. `TalkToSomeoneView` counter-inverts at the call site. When `NavigationLink` pushes Screen 2 or further, those are new navigation pages NOT affected by the root page's `.scaleEffect`. Only the root content needs the flip. + +### `shouldShowOnboarding` and `noConversationChatsYet` + +```swift +private var shouldShowOnboarding: Bool { + !addressCreationCardShown && noConversationChatsYet +} + +private var noConversationChatsYet: Bool { + chatModel.chats.allSatisfy { chat in + switch chat.chatInfo { + case .local: return true + case let .direct(contact): return contact.chatDeleted || contact.isContactCard + case let .group(groupInfo, _): return groupInfo.chatDeleted + case let .contactRequest(req): return req.chatDeleted + case let .contactConnection(conn): return conn.chatDeleted + case .invalidJSON: return true + } + } +} +``` + +Both are computed properties on `ChatListView`. `noConversationChatsYet` reads `chatModel.chats` which is `@Published` on `ChatModel` (`@EnvironmentObject`). SwiftUI re-evaluates the body when it changes, so `shouldShowOnboarding` is reactive. + +Note: `chatModel.chats` may be empty during initial load (before `APIGetChats` completes). `allSatisfy` on an empty array returns `true`. Combined with `!addressCreationCardShown`, this means the onboarding flashes briefly on app launch for users who have conversations but `chats` hasn't loaded yet. Mitigation: also check `chatModel.chats.isEmpty` and show a loading indicator instead: + +```swift +private var shouldShowOnboarding: Bool { + !addressCreationCardShown && !chatModel.chats.isEmpty && noConversationChatsYet +} +``` + +When `chats` is empty (loading), neither onboarding nor chat list shows — the existing loading state (if any) handles it. + +### Auto-dismiss + +`addressCreationCardShown` must be set to `true` when the first real conversation appears, so the onboarding never returns. + +```swift +.onChange(of: chatModel.chats.count) { _ in + if !noConversationChatsYet && !addressCreationCardShown { + addressCreationCardShown = true + } +} +``` + +Placed on `chatList` view. Observes `.count` as a proxy for chat list changes. When count changes and `noConversationChatsYet` is false, the user default is set permanently. This covers: receiving a contact request, establishing a connection, creating a group, etc. + +Edge case: chat count can change without affecting `noConversationChatsYet` (e.g., adding a second note folder). The check `!noConversationChatsYet` prevents unnecessary writes — only sets the default when there's actually a real conversation. + +## User default + +Existing `@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false` at ChatListView line 165. Constant defined in `SettingsView.swift` line 55 as `let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown"`. Also referenced in `AddressCreationCard.swift` line 17 and in `SettingsView.swift` defaults reset (line 114, 144). + +No new user default needed. + +## String localization + +8 new strings for `Localizable.strings` (en). Use `NSLocalizedString` or `LocalizedStringKey` inline — project uses both patterns. + +- "Talk to someone" +- "Let someone connect to you" +- "Connect via link or QR code" +- "Connect with someone" +- "Invite someone privately" +- "A link for one person to connect" +- "Create your public address" +- "For anyone to reach you" + +## Assets + +8 card images in art repo (4 base + 4 light variants). Run `resize.sh` then `copy-assets.sh` to populate `SimpleXAssets.xcassets`. Gated with `#if SIMPLEX_ASSETS`. Without assets: gradient-only cards with labels, still tappable and functional. + +Image base names for the `imageName` parameter: +- Screen 1: `"card-let-someone-connect-to-you-alpha"`, `"card-connect-via-link-alpha"` +- Screen 2: `"card-invite-someone-privately-alpha"`, `"card-create-your-public-address-alpha"` + +The `-light` suffix is appended automatically by `OnboardingCardView` when `colorScheme == .dark`. + +## Files changed + +- `Shared/Views/ChatList/ChatListView.swift` — add `shouldShowOnboarding`, `noConversationChatsYet`, add `@ViewBuilder` to `chatList`, branch to `TalkToSomeoneView`, add `.onChange` for auto-dismiss +- **New:** `Shared/Views/NewChat/OnboardingCards.swift` — `OnboardingCardView`, `TalkToSomeoneView`, `ConnectWithSomeoneView` + +No modifications to NewChatView, UserAddressView, or ConnectView in this phase. + +## Revision 1 — corrections from design review + +### Navigation scope (critical) +Both screens must keep the bottom/top toolbar visible. The onboarding NavigationView is SCOPED to just the card area — it does NOT replace the full chatListView. In `chatList`, wrap `TalkToSomeoneView()` in its own `NavigationView { }.navigationViewStyle(.stack)`. The toolbar from `chatListView.withToolbar()` stays outside and visible on both screens. + +Screen 1 → Screen 2: real NavigationLink push within the scoped NavigationView. +Screen 2 → deeper views: also NavigationLink pushes within same scoped NavigationView. + +### Screen 1 — reserve nav bar space +Screen 2 has a back button (navigation bar). Screen 1 must reserve the same height to prevent content shift on slide. Set `.navigationTitle("")` with `.navigationBarTitleDisplayMode(.inline)` on Screen 1's root — shows an empty inline nav bar matching Screen 2's bar height. + +### Gradient direction fix +Current gradient is nearly horizontal — wrong. Correct angle is 80° CCW from horizontal (almost vertical, slight rightward lean). + +Formula for full-coverage gradient at angle θ: +``` +startPoint = (0.5 - 0.5·cos(θ), 0.5 + 0.5·sin(θ)) +endPoint = (0.5 + 0.5·cos(θ), 0.5 - 0.5·sin(θ)) +``` + +For θ = 80°: `startPoint: .init(x: 0.413, y: 0.992), endPoint: .init(x: 0.587, y: 0.008)` + +### Corner radius +Change from 18 to 24. + +### Label stripe background +The label area has a distinct semi-transparent background strip at the bottom of the card. Add to `labelRow`: +- Light mode: `Color.white.opacity(0.5)` +- Dark mode: `Color.black.opacity(0.3)` +Exact opacity values need visual tuning. + +### Card max height ratio +Cards have a max total height/width ratio of 0.75. On tall screens, cards are capped at this ratio with extra space distributed equally above and below. On short screens, cards shrink — ratio goes below 0.75, label stripe stays fixed height, only image area shrinks. + +Implementation: use `GeometryReader` to get available width, compute `maxCardHeight = cardWidth * 0.75`, apply `.frame(maxHeight: maxCardHeight)` on each card. The VStack centers vertically in the GeometryReader — equal space above and below on tall screens. + +### Title alignment +Change from `.leading` to `.center` — design shows centered titles on both screens. + +### Subtitle color in dark mode +Change `.foregroundColor(.secondary)` to `.foregroundColor(colorScheme == .dark ? .white.opacity(0.7) : .secondary)` — standard `.secondary` is too gray on the dark gradient. + +### Label stripe height proportions +The label stripe has fixed proportional heights relative to card width: +- Screen 1 (single-line labels): 0.132 × card width +- Screen 2 (two-line labels): 0.195 × card width + +These are achieved via fixed padding on the label row. The image area is the remainder of the card height. When cards shrink on short screens, only the image area shrinks — the label stripe stays at its proportional height. + +### Spacing between title and cards, between cards, and below cards +The gaps above first card and below second card should be EQUAL and LARGER than the gap between the two cards. The inter-card gap is the VStack spacing (~16pt). The outer gaps are larger — achieved by the GeometryReader centering the VStack vertically, which distributes extra space equally above and below. + +### ThemedBackground on TalkToSomeoneView +`TalkToSomeoneView` needs `.modifier(ThemedBackground())` — it replaces `chatList` content and needs its own background. Currently missing. + +### `oneHandUI` inversion on Screen 2 +The scoped `NavigationView` sits inside `chatList` which is visually inverted by `chatListView`'s `.scaleEffect(y: -1)`. This inversion applies to the NavigationView's rendered frame — ALL pages inside it (both Screen 1 and Screen 2) are inverted. `TalkToSomeoneView` counter-inverts at the call site. `ConnectWithSomeoneView` (pushed within the NavigationView) also needs counter-inversion. Pass `oneHandUI` as a binding or read from `@AppStorage(GROUP_DEFAULT_ONE_HAND_UI)` directly inside `ConnectWithSomeoneView`, and apply `.scaleEffect(x: 1, y: oneHandUI ? -1 : 1)` on its root VStack. Same for any deeper pushed views — but those are existing views not modified in this phase, so their inversion behavior needs testing. + +### Plan cleanup note +The original sections above contain outdated code snippets (wrong gradient, wrong corner radius, wrong switch cases, wrong alignment). The Revision 1 sections are authoritative. When implementing, follow Revision 1 values; treat original sections as structural context only. + diff --git a/plans/2026-04-10-relay-leaving-group.md b/plans/2026-04-10-relay-leaving-group.md new file mode 100644 index 0000000000..42aebf425b --- /dev/null +++ b/plans/2026-04-10-relay-leaving-group.md @@ -0,0 +1,234 @@ +# Plan: Relay Leaving Group (Moderation Capability) + +## Context + +SimpleX Chat channels use chat relays to forward messages from owners to subscribers. When a channel hosts prohibited content, the relay operator needs the ability to make their relay leave the group. Currently `APILeaveGroup` doesn't work correctly for relay members: the `getRecipients` helper always uses `getGroupRelayMembers` for channel groups, which returns only other relays (members with `GRRelay` role) — the owner is excluded. The relay needs to notify the owner (so it can update channel link data) and all subscribers directly (relay has connections to all of them). + +## Flow + +1. **Relay** calls `APILeaveGroup` → sends `XGrpLeave` directly to all members (owners + subscribers) → deletes all connections +2. **Owner** receives `XGrpLeave` → updates relay member status to `GSMemLeft` → updates `GroupRelay.relayStatus` to `RSInactive` → updates channel link relay list via `updatePublicGroupData` → `setGroupLinkDataAsync` → `setAgentConnShortLinkAsync` (excludes left relay from link data) → relay bar shows status +3. **Subscribers** receive `XGrpLeave` directly from relay → update relay member status to `GSMemLeft` → delete connection to relay → relay bar shows status + +## Changes + +### 1. Add `RSInactive` to `RelayStatus` + +**`src/Simplex/Chat/Types/Shared.hs`** (~L81-112) + +`GroupMemberStatus` already carries left/removed semantics (`GSMemLeft`, `GSMemRemoved`), so `RelayStatus` should not duplicate that. Add `RSInactive` as a generic terminal status meaning "no longer operational", complementing `RSActive`. Add `"inactive"` encoding in `relayStatusText` and `TextEncoding` instance. + +```haskell +data RelayStatus + = RSNew + | RSInvited + | RSAccepted + | RSActive + | RSInactive + deriving (Eq, Show) +``` + +### 2. Fix `APILeaveGroup` recipients for relay + +**`src/Simplex/Chat/Library/Commands.hs`** (~L2838-2844) + +Use nested condition inside `useRelays'` guard. When relay leaves, it sends `XGrpLeave` to all current/pending members directly (relay has connections to all of them). + +```haskell +getRecipients user gInfo@GroupInfo {membership} + | useRelays' gInfo = + if isRelay membership + then do + -- Relay leaving: notify all members directly, clean up all connections + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + pure (ms, filter memberCurrentOrPending ms) + else do + relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + pure (relays, relays) + | otherwise = do + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + pure (ms, filter memberCurrentOrPending ms) +``` + +- `members` (first tuple) = all members → used by `deleteMembersConnections'` for connection cleanup +- `recipients` (second tuple) = all current/pending members → XGrpLeave sent directly + +Existing functions: `isRelay` (Types.hs:1063), `getGroupMembers` (Store/Groups.hs), `memberCurrentOrPending` (Types.hs:1308). + +### 3. Update `xGrpLeave` to set relay status and channel link on owner + +**`src/Simplex/Chat/Library/Subscriber.hs`** (~L3113-3124) + +After `updateMemberRecordDeleted` and before `updatePublicGroupData`, set `RSInactive` on the owner's `GroupRelay` record. On subscribers this is a no-op because they have no `GroupRelay` record (`getGroupRelayByGMId` returns `Left`, `forM_` skips). + +```haskell +xGrpLeave gInfo m msg@RcvMessage {msgSigned} brokerTs = do + deleteMemberConnection m + gInfo' <- updateMemberRecordDeleted user gInfo m GSMemLeft + -- Set relay status to inactive (owner-only; subscriber has no GroupRelay record) + when (isRelay m) $ + withStore' $ \db -> do + relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m) + forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive + gInfo'' <- updatePublicGroupData user gInfo' + -- ... rest unchanged +``` + +The channel link update chain on owner: `updatePublicGroupData` (Internal.hs:1317) → `setGroupLinkDataAsync` (Internal.hs:1309) → `getConnectedGroupRelays` (filters `member_status = GSMemConnected AND relay_status IN (RSAccepted, RSActive)`) → `groupLinkData` (builds `UserContactLinkData` with remaining relay links only) → `setAgentConnShortLinkAsync` (updates SMP short link). The left relay is excluded by the `member_status` filter, so its link is removed from the channel link data. + +Existing functions: `isRelay` (Types.hs:1063), `getGroupRelayByGMId` (Store/Groups.hs:1296), `updateRelayStatus` (Store/Groups.hs:1418), `groupMemberId'` (Types.hs). + +### 4. Client type updates + +**`apps/ios/SimpleXChat/ChatTypes.swift`** (~L2558-2563, L2628-2637) + +Add `case rsInactive = "inactive"` to `RelayStatus` enum and `case .rsInactive: "inactive"` to `text` property. + +**`apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt`** (~L2266-2276) + +Add `@SerialName("inactive") RsInactive` to `RelayStatus` enum and `RsInactive -> generalGetString(MR.strings.relay_status_inactive)` to `text` property. + +**`apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml`** (~L2879) + +Add `inactive`. + +**`packages/simplex-chat-client/types/typescript/src/types.ts`** (~L3608-3613) + +Add `Inactive = "inactive"` to `RelayStatus` enum. + +### 5. UI: relay bar updates + +The relay bar (above compose area) currently shows for owners when `activeCount < relays.count`, and for subscribers in steady state. After relay leaves, the bar shows but needs better indication of what happened and whether delivery is broken. + +#### 5a. Fix `relayStatusIndicator` color for `RSInactive` + +Currently `relayStatusIndicator` shows yellow for any non-active status — yellow implies "connecting/in progress", which is wrong for an inactive relay. + +**iOS** (`apps/ios/Shared/Views/NewChat/AddChannelView.swift` L431-432): +```swift +// CURRENT: +let color: Color = connFailed ? .red : (status == .rsActive ? .green : .yellow) +// NEW: +let color: Color = connFailed ? .red : (status == .rsActive ? .green : (status == .rsInactive ? .red : .yellow)) +``` + +**Kotlin** (`apps/multiplatform/.../views/newchat/AddChannelView.kt` L551): +```kotlin +// CURRENT: +val color = if (connFailed) Color.Red else if (status == RelayStatus.RsActive) Color.Green else WarningYellow +// NEW: +val color = if (connFailed) Color.Red else when (status) { + RelayStatus.RsActive -> Color.Green + RelayStatus.RsInactive -> Color.Red + else -> WarningYellow +} +``` + +#### 5b. Owner relay bar: "no active relays" message + +When `activeCount == 0`, show a warning in the relay bar that delivery is broken and adding new relays is coming. + +**iOS** (`apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift` ~L730-738): + +In `ownerChannelRelayBar`, when expanded and `activeCount == 0`, add footer text: +```swift +"Messages can't be delivered to subscribers. Adding new relay will be available in a future update." +``` + +**Kotlin** (`apps/multiplatform/.../views/chat/ComposeView.kt` ~L1647-1657): same logic. + +New string: `relay_bar_owner_no_delivery` = "Messages can't be delivered to subscribers. Adding new relay will be available in a future update." + +#### 5c. Subscriber relay bar: show disconnection in steady state + +Currently in steady state (`showProgress = false`), the subscriber relay bar header shows only "N relays" with no error indication. When relay connections are deleted (relay left), the subscriber sees no issue in collapsed view. + +**iOS** (`apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift` ~L780-792): + +When `!showProgress`, check error state: +```swift +if !showProgress { + if errorCount == total { + Text("All relays disconnected – messages can't be delivered") + } else if errorCount > 0 { + Text(String.localizedStringWithFormat("%d/%d relays connected, %d errors", connectedCount, total, errorCount)) + } else { + Text(String.localizedStringWithFormat("%d relays", total)) + } +} +``` + +**Kotlin** (`apps/multiplatform/.../views/chat/ComposeView.kt` ~L1695-1707): same logic. + +New strings: +- `relay_bar_all_disconnected` = "All relays disconnected – messages can't be delivered" +- `relay_bar_connected_with_errors_steady` = "%1$d/%2$d relays connected, %3$d errors" + +### 6. Test + +**`tests/ChatTests/Groups.hs`** + +Add `testChannelRelayLeave` test: + +1. Create channel with 2 relays (`relay1`, `relay2`) and 2 subscribers (`dan`, `eve`) via `prepareChannel2Relays` + `memberJoinChannel` +2. Verify channel works: owner sends message → subscribers receive via relay forwarding +3. `relay1` leaves: `relay1 ##> "/leave #team"` +4. Verify relay1 output: `"#team: you left the group"` +5. Verify owner output: `"#team: left the group (signed)"` +6. Verify subscribers receive `XGrpLeave` directly — check relay1 member status is `"left"` on subscribers via `checkMemberStatus` +7. Wait for async link data update +8. Verify channel still works with remaining relay: owner sends message → relay2 forwards → subscribers receive +9. `relay2` leaves: `relay2 ##> "/leave #team"` +10. Verify relay2 output and owner/subscriber leave events +11. **Verify no delivery**: owner sends message, `threadDelay`, check subscribers' last item is still the previous message (not the new one) — pattern from `testChannelSubscriberLeave` L9237 + +Register test in test list at ~L261 (after `testChannelSubscriberLeave`). + +## Files Modified + +| File | Change | +|------|--------| +| `src/Simplex/Chat/Types/Shared.hs` | Add `RSInactive` to `RelayStatus` | +| `src/Simplex/Chat/Library/Commands.hs` | Fix `getRecipients` for relay in `APILeaveGroup` | +| `src/Simplex/Chat/Library/Subscriber.hs` | Update `xGrpLeave` to set relay status | +| `apps/ios/SimpleXChat/ChatTypes.swift` | Add `rsInactive` case | +| `apps/ios/Shared/Views/NewChat/AddChannelView.swift` | Fix `relayStatusIndicator` color for inactive | +| `apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift` | Owner/subscriber relay bar messages | +| `apps/multiplatform/.../ChatModel.kt` | Add `RsInactive` case | +| `apps/multiplatform/.../AddChannelView.kt` | Fix `RelayStatusIndicator` color for inactive | +| `apps/multiplatform/.../ComposeView.kt` | Owner/subscriber relay bar messages | +| `apps/multiplatform/.../strings.xml` | Add `relay_status_inactive` + relay bar strings | +| `packages/.../types.ts` | Add `Inactive` to enum | +| `tests/ChatTests/Groups.hs` | Add `testChannelRelayLeave` test | + +## Verification + +```bash +cabal build --ghc-options=-O0 +cabal test simplex-chat-test --test-options='-m "channels"' +``` + +Manual: verify relay bar appearance on iOS simulator and Android emulator after relay leaves. + +## Adversarial Review + +**Pass 1:** +- Relay's `sendGroupMessage'` signs `XGrpLeave` (`requiresSignature XGrpLeave_ = True`; relay has `groupKeys` with `memberPrivKey`). Owner and subscribers verify signature. OK. +- `deleteMembersConnections' user members True` with `waitDelivery=True` ensures `XGrpLeave` reaches SMP queues before relay deletes connections. OK. +- Owner's channel link update: `updatePublicGroupData` → `setGroupLinkDataAsync` → `getConnectedGroupRelays` (excludes left relay by `member_status = GSMemLeft`) → `groupLinkData` (builds link with remaining relays) → `setAgentConnShortLinkAsync` (updates SMP short link). Left relay's link removed. OK. +- `muteEventInChannel` for relay member (`GRRelay < GRModerator`): muted for subscribers (no chat item), but DB is updated. Owner sees event (`GROwner >= GRModerator`). OK. +- Relay's `deleteGroupLinkIfExists` is no-op (relay doesn't own group link). OK. +- `getGroupRelayByGMId` on subscriber returns `Left` (no `GroupRelay` record) → `forM_` skips. No error. OK. +- `memberEventDeliveryScope` returns `DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}` for relay member. On subscriber, creates a delivery task but subscriber has no forwarding role — delivery worker finds no eligible connections. Harmless no-op. OK. +- Owner relay bar: `activeCount` drops (RSInactive ≠ RSActive) → bar shows → `relayStatusIndicator` shows red dot with "inactive". When `activeCount == 0`: "Messages can't be delivered... Adding new relay will be available in a future update." OK. +- Subscriber relay bar: `connStatus = .deleted` → `deletedCount` increases → `errorCount` increases. When `errorCount == total` in steady state: "All relays disconnected – messages can't be delivered". OK. + +**Pass 2:** +- Race condition: owner removes relay + relay leaves simultaneously. Both paths delete connection and update member status. No data corruption — idempotent. `GroupRelay.relayStatus` ends as `RSInactive` from `xGrpLeave` or unchanged from `xGrpMemDel` (which doesn't update relay status). OK. +- `getGroupMembers` for relay may return thousands of subscribers. `deleteMembersConnections'` uses `deleteAgentConnectionsAsync'` which handles batching. `sendGroupMessage'` also handles sending to many members. OK. +- Relay's own `membership` record has no `activeConn` in members list (can't connect to self). `mapMaybe` in `deleteMembersConnections'` filters it out. OK. +- Both relays leave: after last relay leaves, owner sends message. Delivery system has no connected relays to forward through — message saved locally but not delivered. Subscribers' last chat item remains unchanged. Test verifies this. OK. +- `getGroupRelays` (used by `apiGetGroupRelays`) returns ALL GroupRelay records including RSInactive — owner UI correctly includes left relays in bar. OK. +- Subscriber `groupMembers` filter (`memberRole == .relay`) includes left members (memberRole unchanged) — subscriber UI correctly shows left relays. OK. + +**Result: 2 consecutive clean passes.** diff --git a/plans/2026-04-11-channel-invitations-directory.md b/plans/2026-04-11-channel-invitations-directory.md new file mode 100644 index 0000000000..6ae2c046db --- /dev/null +++ b/plans/2026-04-11-channel-invitations-directory.md @@ -0,0 +1,255 @@ +# Public Group Invitations & Directory Listing + +## Goal + +Enable public group (channel) subscribers to invite new subscribers by sharing a channel card in any chat where they can send messages. Channel owners can prove ownership via a signed card. This unblocks directory service support for public groups alongside regular groups. + +Sharing channels should be as simple as forwarding — share button on channel opens chat picker, sends a channel card as a regular message. Old clients show the text; new clients show a rich card with profile and join button. + +## Context + +### Current state +- Public groups have `PublicGroupProfile {groupType = GTChannel, groupLink, publicGroupId}` and `useRelays = True` +- Users join public groups via link → `APIPrepareGroup` → `APIConnectPreparedGroup` +- `MCChat` message content exists with `MsgChatLink` variants for contacts, invitations, and groups (`MCLGroup`) +- Group invitations (`XGrpInv`) carry `connRequest :: ConnReqInvitation` — public groups don't use this mechanism +- Directory bot registers groups via group invitation (owner invites bot as admin) — public groups need a different flow + +### Owner keys in public group links +- `FixedLinkData.rootKey :: PublicKeyEd25519` — genesis root key +- `UserContactData.owners :: [OwnerAuth]` — chain of authorized owner keys, each signed by root or previous owner +- Public group creator stores `GroupKeys {groupRootKey = GRKPrivate rootPrivKey, memberPrivKey}` +- `memberPrivKey`'s public key = `ownerKey` in the `OwnerAuth` entry (created via `newOwnerAuth`) +- `publicGroupId = sha256(rootPubKey)` — immutable group identity + +### DR connection shared secret +- Each direct connection has `rcAD` (Associated Data) from X3DH key exchange +- `getConnectionRatchetAdHash` returns `sha256(rcAD)` — binding for replay protection + +## Design + +### Channel cards as MCChat messages + +Channel invitations are sent as regular `XMsgNew` with `MCChat` content. No new protocol messages. + +```haskell +data MsgContent + = ... + | MCChat {text :: Text, chatLink :: MsgChatLink, ownerSig :: Maybe LinkOwnerSig} + | ... +``` + +`ownerSig` is optional. Old clients ignore it (missing field) and show `text` as a regular message. + +```haskell +data LinkOwnerSig = LinkOwnerSig + { ownerId :: Maybe OwnerId, -- Nothing = root key, Just = owner key from OwnerAuth chain + binding :: B64UrlByteString, + ownerSig :: B64UrlByteString + } +``` + +Sending is supported for channel cards only (for now). Verification is generic for all `MsgChatLink` types: +- `ownerId = Just id`: verified against matching `OwnerAuth.ownerKey` in the link's owner chain (channels) +- `ownerId = Nothing`: verified against `rootKey` from `FixedLinkData` (contacts, invitations) + +The sender proves control over the link regardless of type. + +### What is signed + +`smpEncode chatBinding <> bindingData <> smpEncode chatLink` signed with `memberPrivKey`. + +Binding depends on where the card is sent: +- **Direct chat**: `CBDirect` with `ratchetAdHash` +- **Public group**: `CBGroup` with `smpEncode (publicGroupId, memberId)` +- **Group without public identity**: signature treated as failed at verification time + +Binding is to chat, not to message (`sharedMsgId` is not included). This allows the sender to forward their own signed card within the same chat (e.g., re-sharing a channel link as a reminder) without invalidating the signature. Message-level binding would prevent this since forwarded messages get new `sharedMsgId`s. + +### Sending flow + +1. User presses "Share" on channel → API call `APIPrepareLinkOwnerSig GroupId` returns `Maybe LinkOwnerSig` +2. Opens chat picker (same as forwarding) — chats with disabled simplex links greyed out +3. Sends `XMsgNew` with `MCChat {text = displayName, chatLink = MCLGroup {connLink, groupProfile}, ownerSig}` +4. Creates regular `CISndMsgContent` chat item — no new item types, no new response types + +### Receiving flow + +Regular `XMsgNew` processing. Creates `CIRcvMsgContent (MCChat ...)`. No hidden groups, no async verification, no special events. + +UI renders channel card with profile, member count, join button. If `ownerSig` present, shows "signed by owner" indicator (unverified until join). + +### Verification at join time + +When user taps "Join" on a channel card: + +1. UI extracts `connLink` and `ownerSig` from `MCChat` message content +2. UI calls `APIConnectPlan` with the link and signature. `APIConnectPlan` extended: + ```haskell + APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, linkOwnerSig :: Maybe LinkOwnerSig} + ``` + Parser: `/_connect plan [sig=]` +3. Inside `connectPlan`, if `linkOwnerSig` is present: + - Gets `FixedLinkData {rootKey}` and `UserContactData {owners}` from resolved link + - Finds verification key: `ownerId = Nothing` → `rootKey`, `ownerId = Just id` → matching `OwnerAuth.ownerKey` + - Verifies binding data against expected value from context + - Verifies signature +4. Each "OK" plan variant extended with verification result: + ```haskell + data LinkSigVerification = LSVVerified | LSVFailed {reason :: Text} + + ILPOk {contactSLinkData_, linkSigVerification :: Maybe LinkSigVerification} + CAPOk {contactSLinkData_, linkSigVerification :: Maybe LinkSigVerification} + GLPOk {groupSLinkInfo_, groupSLinkData_, linkSigVerification :: Maybe LinkSigVerification} + -- Nothing = not signed, Just LSVVerified = verified, Just LSVFailed = failed with reason + ``` + Reasons: "unknown owner ID", "binding data mismatch", "signature verification failed", "no group identity for verification" +5. UI shows verification result in join/connect alert for the OK plan variants +6. User confirms → `APIPrepareGroup` → `APIConnectPreparedGroup` — existing join flow, no changes + +Pasted links (no message context) pass `linkOwnerSig = Nothing` — plan shows "not signed." + +### Forwarding + +When `MCChat` is forwarded, `ownerSig` is dropped — UNLESS forwarded by sender in the same chat (re-sharing own card as reminder). Signature is bound to chat context, so forwarding in the same chat preserves validity. + +Implementation: in forwarding code, drop `ownerSig` unless `fromChatRef == toChatRef` and sender is the same user. + +### Simplex link permission + +`MCChat` IS a simplex link — if `SGFSimplexLinks` is prohibited for the sender's role, `MCChat` should be prohibited regardless of content. + +Currently `prohibitedSimplexLinks` (Internal.hs:363) only checks formatted text. Fix: also check `MsgContent` type — if it's `MCChat` and simplex links are not allowed, prohibit it. This covers both send and receive via existing `prohibitedGroupContent` calls. + +For backward compatibility, the current text-level check is sufficient since the link is included in `text`. But the `MCChat` type check is the correct long-term fix. + +### CLI view + +`MCChat` with `MCLGroup` renders as channel card with display name. If `ownerSig` present, shows "(signed)" indicator. + +## Directory bot changes + +### Registration flow + +Bot receives regular `CIRcvMsgContent (MCChat ...)` messages in direct chat from channel owners. Bot checks `ownerSig` is present. Verifies at join time via `connectPlan`. No special events needed. + +- Owner sends channel card to bot in DM (signed) +- Bot resolves link, verifies owner signature +- Bot joins channel as subscriber +- Simplified approval flow: `GRSProposed` → `GRSPendingApproval` → `GRSActive` + +### Profile monitoring + +Bot as subscriber receives `XGrpInfo` when owner updates profile. On profile change: re-resolve link, compare. Periodic re-verification. + +### Search and listing + +Search includes both groups and public groups. No separate listing category — `groupProfile.publicGroup` is the source of truth. `DETGroup` works for both in JSON listing. + +## Implementation plan (diff from master) + +### Step 1: LinkOwnerSig type + +- `LinkOwnerSig` type in Types.hs (or Protocol.hs alongside `MCChat`) +- `ownerSig :: Maybe LinkOwnerSig` field on `MCChat` +- JSON derivation with backward compat (optional field) + +### Step 2: CBDirect + +- Add `CBDirect` to `ChatBinding` in Protocol.hs (already done on master via refactoring PR) + +### Step 3: Share chat message content API + +New command that constructs the complete `MCChat` content for sharing: +```haskell +-- Controller.hs +APIShareChatMsgContent {shareChatRef :: ChatRef, toChatRef :: ChatRef} +-- returns CRChatMsgContent {user :: User, msgContent :: MsgContent} +``` + +Implementation in Commands.hs: +1. Load shared chat info from `shareChatRef` — initially only `CTGroup` with public groups supported +2. Get `PublicGroupProfile {groupLink}` and `groupProfile` from group +3. Determine if user is owner (has `GroupKeys {memberPrivKey}`) +4. If owner, compute binding based on `toChatRef`: + - `ChatRef CTDirect contactId` → `getConnectionRatchetAdHash` on contact's connection → `CBDirect` + - `ChatRef CTGroup groupId` → `smpEncode (publicGroupId, memberId)` if group has identity → `CBGroup` + - Group without identity → `Nothing` (can't sign) +5. If owner and binding available, sign `smpEncode chatBinding <> bindingData <> smpEncode chatLink` with `memberPrivKey` +6. Return `MCChat {text = displayName, chatLink = MCLGroup {connLink = groupLink, groupProfile}, ownerSig}` + +Parser: `/_share_chat ` + +UI flow: press Share on channel → chat picker → select destination → call `APIShareChatMsgContent` → get `MsgContent` → send via existing `APISendMessages` + +All business logic (ownership check, signing decision, link extraction, profile inclusion) stays in core. UI only passes two chat refs and sends the returned content. + +### Step 4: connectPlan verification + +Extend `APIConnectPlan` (Controller.hs:472): +```haskell +APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, linkOwnerSig :: Maybe LinkOwnerSig} +``` + +Parser (Commands.hs:4945): extend to accept optional JSON `LinkOwnerSig` parameter. + +In `connectPlan` (Commands.hs), pass `linkOwnerSig` to `groupShortLinkPlan` / `groupJoinRequestPlan`. + +In `groupShortLinkPlan` (Commands.hs ~line 3944): after resolving the link via `getShortLinkConnReq`, if `linkOwnerSig` is present: +1. Extract `FixedLinkData {rootKey}` and `UserContactData {owners}` +2. If `ownerId = Nothing`: verify against `rootKey` +3. If `ownerId = Just id`: find `OwnerAuth` where `ownerId == id`, verify against `ownerKey` +4. Check binding data matches expected +5. Verify signature + +Extend `GroupLinkPlan` (Controller.hs:1025): +```haskell +GLPOk {groupSLinkInfo_, groupSLinkData_, ownerVerified :: Maybe Bool} +``` +`Nothing` = not signed, `Just True` = verified, `Just False` = failed. + +`CRConnectionPlan` response carries this through to UI — shown in plan alert. + +### Step 5: Forwarding — drop ownerSig + +In message forwarding code (Commands.hs, `APIForwardChatItems`), when forwarding `MCChat` content, set `ownerSig = Nothing`. + +Location: Commands.hs where forwarded message content is constructed — find where `MCChat` is handled in forwarding and strip the signature. + +### Step 6: Permission check + +Fix `prohibitedSimplexLinks` (Internal.hs:363) to also check `MsgContent` type — if `MCChat`, treat as simplex link. Covers both send and receive paths via existing `prohibitedGroupContent` calls. + +For backward compatibility, the link is also in `text` field, so existing text-level check catches it. The type check is the correct fix. + +### Step 7: CLI view + +In `viewChatItem` (View.hs), `MCChat` content already renders via `ttyMsgContent`. Extend to show channel card format and "(signed)" indicator when `ownerSig` is present. + +### Step 8: groupLinkData owners preservation + +Fix `groupLinkData` (Internal.hs:1330) to reconstruct `OwnerAuth` from `GroupKeys` instead of hardcoding `owners = []`. This ensures the resolved link data has the owner keys needed for verification. + +Implementation: when `GroupKeys` has `GRKPrivate rootPrivKey` and `memberPrivKey`, reconstruct `OwnerAuth` with `ownerId = unMemberId memberId`, `ownerKey = publicKey memberPrivKey`, `authOwnerSig = sign rootPrivKey (ownerId <> encodePubKey ownerKey)`. + +### Step 9: Tests + +- Share channel card in direct chat (owner signed) +- Share channel card in group (unsigned — no binding for groups without identity) +- Share channel card in channel +- Join via channel card — verify `connectPlan` shows verification result +- Non-public group share rejected +- Forwarded card has no signature +- Old client compatibility (text field shown) + +### Step 10: Directory bot + +- Handle `MCChat` with `MCLGroup` in `crDirectoryEvent_` +- Channel registration flow +- Profile monitoring + +## What stays from refactoring PR (already on master) + +- `CBDirect` in `ChatBinding` +- `HasShortLink` typeclass with `connShortLink'` +- `setShortLinkType` / `setShortLinkType_` diff --git a/plans/2026-04-16-ios-share-channel-link.md b/plans/2026-04-16-ios-share-channel-link.md new file mode 100644 index 0000000000..414016c9c4 --- /dev/null +++ b/plans/2026-04-16-ios-share-channel-link.md @@ -0,0 +1,407 @@ +# iOS — Share chat card (MCChat) + +Share a public group/channel link as a card in any chat. Backend (`APIShareChatMsgContent`, `SharePublicGroup`) exists. This plan covers: send-side UI (picker, compose, send), receive-side UI (card rendering, tap-to-connect with owner verification), and the plumbing between. + +--- + +## 0. UX flow + +1. Channel info screen (`GroupChatInfoView`): two entry points. + - **Quick-access action button** row (next to search/mute): "share" button — visible when `groupInfo.useRelays` and channel has a public link. Non-owners see it too. + - **Section button**: "Share via chat" inside the existing channel-link section (after existing system "Share link" button). +2. Tap either → **destination picker sheet** (reused from `ChatItemForwardingView` by parameterization, NOT a new view). +3. Tap a destination → sheet dismisses, navigates to the destination chat, compose shows **plaque** above input: "Sharing #channelName" (reused `ContextItemView` by parameterization, NOT a new view). +4. User may type optional text. Tap Send. +5. Backend builds MCChat: `text = \n` (link appended for old clients), signed with owner key if applicable. Sent as one message. +6. **Receive side**: card renders as a group-invitation-style tile (reused from `CIGroupInvitationView` by extracting shared component, NOT copy-pasted). Shows profile image + channel name + icon per link type + "from channel owner" if `ownerSig` present. Tap → `planAndConnect` flow with `linkOwnerSig` → alert shows owner verification result alongside standard plan info. + +--- + +## B. Implementation, file by file + +### 1. `SimpleXChat/ChatTypes.swift` — `LinkOwnerSig`, `OwnerVerification`, `MsgContent.chat` update + +**LinkOwnerSig** (new struct, near `MsgChatLink` ~line 4790): +```swift +public struct LinkOwnerSig: Codable, Equatable, Hashable { + public let ownerId: String? + public let chatBinding: String + public let ownerSig: String +} +``` + +**OwnerVerification** (new enum, near `GroupLinkPlan` in AppAPITypes.swift ~line 1379): +```swift +enum OwnerVerification: Decodable, Hashable { + case verified + case failed(reason: String) +} +``` + +**MsgContent.chat** — add `ownerSig` field: +- Case: `case chat(text: String, chatLink: MsgChatLink, ownerSig: LinkOwnerSig?)` +- CodingKeys: add `case ownerSig` +- Decoder (4719-4722): add `let ownerSig = try container.decodeIfPresent(LinkOwnerSig.self, forKey: .ownerSig)` +- Encoder (4764-4767): add `try container.encodeIfPresent(ownerSig, forKey: .ownerSig)` +- text getter (4605): `case let .chat(text, _, _)` +- `==` (4679): `case let (.chat(lt, ll, ls), .chat(rt, rl, rs)): return lt == rt && ll == rl && ls == rs` +- ComposeView.swift:1480: `case let .chat(_, chatLink, ownerSig): return .chat(text: msgText, chatLink: chatLink, ownerSig: ownerSig)` + +### 2. `Shared/Model/AppAPITypes.swift` — `SendRef`, plan types, command, response + +**SendRef** (new enum, near `ref()` helper ~line 580): +```swift +enum SendRef { + case direct(contactId: Int64) + case group(groupId: Int64, scope: GroupChatScope?, asGroup: Bool) +} + +func sendRef(_ r: SendRef) -> String { + switch r { + case let .direct(contactId): "@\(contactId)" + case let .group(groupId, scope, asGroup): + "#\(groupId)\(scopeRef(scope))\(asGroup ? "(as_group=on)" : "")" + } +} +``` + +**Plan types — add `ownerVerification`** to `.ok` cases: +- `InvitationLinkPlan.ok` (1352): `case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?)` +- `ContactAddressPlan.ok` (1359): `case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?)` +- `GroupLinkPlan.ok` (1374): `case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?)` + +**ChatCommand** — new case after `apiForwardChatItems` (line 64): +```swift +case apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toSendRef: SendRef) +``` +cmdString: `"/_share chat content \(ref(shareChatType, shareChatId, scope: nil)) \(sendRef(toSendRef))"` + +**APIConnectPlan** — extend with `linkOwnerSig`: +- Current (line ~150ish): `case apiConnectPlan(userId: Int64, connLink: String?)` +- Change to: `case apiConnectPlan(userId: Int64, connLink: String?, linkOwnerSig: LinkOwnerSig?)` +- cmdString: append `linkOwnerSig.map { " sig=" + encodeJSON($0) } ?? ""` (matches Haskell parser `optional (" sig=" *> jsonP)`) + +**ChatResponse1** — new case after `newChatItems` (823): +```swift +case chatMsgContent(user: UserRef, msgContent: MsgContent) +``` +Plus `responseType` + `details` entries. + +### 3. `Shared/Model/SimpleXAPI.swift` — wrappers + +**apiShareChatMsgContent** (near apiForwardChatItems, line 506): +```swift +func apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toSendRef: SendRef) async throws -> MsgContent { + let r: APIResult = await chatApiSendCmd( + .apiShareChatMsgContent(shareChatType: shareChatType, shareChatId: shareChatId, toSendRef: toSendRef) + ) + if case let .result(.chatMsgContent(_, mc)) = r { return mc } + throw r.unexpected +} +``` + +**apiConnectPlan** (line 1023-1032): add `linkOwnerSig: LinkOwnerSig? = nil` parameter, pass to `.apiConnectPlan(userId:connLink:linkOwnerSig:)`. + +### 4. `SimpleXChat/ChatUtils.swift` — NO new filter function + +Reuse `filterChatsToForwardTo` + `canForwardToChat` as-is. Chat cards ARE simplex links, so `prohibitedByPref(hasSimplexLink: true, ...)` applies and correctly gates by the destination's simplex-links preference + user's role. No separate filter. + +### 5. `ChatItemForwardingView.swift` — parameterize for dual use + +Add parameters to support both forwarding and sharing modes. **No new view file.** + +Add to the struct: +```swift +var title: String = "Forward" +var chats: [Chat] // caller provides filtered list (replaces internal filterChatsToForwardTo) +var isProhibited: ((Chat) -> Bool)? = nil // default: existing prohibitedByPref check; sharing overrides +var onSelect: (Chat) -> Void // replaces the inline tap handler +``` + +Remove `chatItems`, `fromChatInfo`, and `composeState` — the `onSelect` closure captures whatever the caller needs. The caller builds `ComposeState` and does navigation externally. + +Existing forwarding call site (`ChatView.swift:278`) adapts: +```swift +ChatItemForwardingView( + title: "Forward", + chats: filterChatsToForwardTo(chats: chatModel.chats), + isProhibited: { chat in forwardedChatItems.map { ci in chat.prohibitedByPref(...) }.contains(true) }, + onSelect: { chat in + dismiss forwarding sheet + set composeState to forwarding context + if different chat: loadOpenChat(chat.id) + } +) +``` + +Sharing call site (from GroupChatInfoView): +```swift +ChatItemForwardingView( + title: "Share channel", + chats: filterChatsToForwardTo(chats: chatModel.chats), // same filter + isProhibited: { chat in chat.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelect: { chat in + dismiss info sheet + set composeState to .sharingChatCard(sourceGroupInfo) + if different chat: loadOpenChat(chat.id) + } +) +``` + +### 6. `ContextItemView.swift` — parameterize for chat-card context + +Add an optional `customText: String?` property. When set, render that text instead of the ChatItem preview. Everything else (icon, cancel button, background, layout) stays the same. + +```swift +var customText: String? = nil // e.g., "Sharing #news" +``` + +When `customText != nil`: +- Display the string in place of the `msgContentView` / multi-message count +- Use `Color(uiColor: .tertiarySystemBackground)` for background (no ChatItem to derive color from) + +ComposeView's `contextItemView()` dispatch for the new case: +```swift +case let .sharingChatCard(sourceGroupInfo): + ContextItemView( + chat: chat, + contextItems: [], + contextIcon: "arrowshape.turn.up.forward", + cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }, + customText: "Sharing #\(sourceGroupInfo.groupProfile.displayName)" + ) + Divider() +``` + +### 7. `ComposeView.swift` — new context case + send dispatch + +**New `ComposeContextItem` case** (line 20-26): +```swift +case sharingChatCard(sourceGroupInfo: GroupInfo) +``` +Name is `sharingChatCard` (not channel-specific — MCChat is general). + +**Convenience init** (after `forwardingItems` init at 90-96): +```swift +init(sharingChatCard sourceGroupInfo: GroupInfo) { + self.message = "" + self.parsedMessage = [] + self.preview = .noPreview + self.contextItem = .sharingChatCard(sourceGroupInfo: sourceGroupInfo) + self.voiceMessageRecordingState = .noRecording +} +``` + +**Accessor** (after `forwarding` at 146-150): +```swift +var sharingChatCard: Bool { + switch contextItem { + case .sharingChatCard: return true + default: return false + } +} +``` + +**sendEnabled** (176): add `|| sharingChatCard`. + +**Draft-restore guard** (`ChatView.swift:758`): extend `!composeState.forwarding` to `!composeState.forwarding && !composeState.sharingChatCard`. + +**Send dispatch** in `sendMessageAsync` (before forwarding branch at 1354): +```swift +if case let .sharingChatCard(sourceGroupInfo) = composeState.contextItem { + sent = await shareChatCard(sourceGroupInfo, ttl) +} else if case let .forwardingItems(...) = ... { +``` + +**Helper** inside the same scope: +```swift +func shareChatCard(_ sourceGroupInfo: GroupInfo, _ ttl: Int?) async -> ChatItem? { + let toSendRef: SendRef + switch chat.chatInfo { + case let .direct(contact): + toSendRef = .direct(contactId: contact.contactId) + case let .group(gInfo, scope): + toSendRef = .group(groupId: gInfo.groupId, scope: scope, asGroup: gInfo.useRelays) + default: + return nil + } + do { + var mc = try await apiShareChatMsgContent( + shareChatType: .group, shareChatId: sourceGroupInfo.groupId, toSendRef: toSendRef + ) + // Append user-typed text: backend returns MCChat with text=link; prepend user message if present + if !composeState.message.isEmpty, case let .chat(text, chatLink, ownerSig) = mc { + mc = .chat(text: composeState.message + "\n" + text, chatLink: chatLink, ownerSig: ownerSig) + } + return await send(mc, quoted: nil, live: false, ttl: ttl, mentions: [:]) + } catch { + logger.error("shareChatCard failed: \(error.localizedDescription)") + return nil + } +} +``` + +**Post-send draft-restore** (1411-1417): mirror `wasForwarding` with `wasSharing`. + +### 8. `GroupChatInfoView.swift` — two entry points + composeState plumbing + +**Add to struct**: `@Binding var composeState: ComposeState` and `@State private var showSharePicker = false`. + +**Quick-access button** — in `infoActionButtons()` (line 354-370), add after `channelLinkActionButton` / `addMembersActionButton` branch: +```swift +if groupInfo.useRelays && groupInfo.groupProfile.publicGroup?.groupLink != nil { + InfoViewButton(image: "arrowshape.turn.up.forward", title: "share", width: buttonWidth) { + showSharePicker = true + } + .disabled(!groupInfo.ready) +} +``` +Adjust the `buttonWidth` divisor accordingly (4 → 5 if all four buttons can show). + +**Section button** — in `if groupInfo.useRelays` Section (line 104-125), after existing "Share link" button (115): +```swift +Button { + showSharePicker = true +} label: { + Label("Share via chat", systemImage: "arrowshape.turn.up.forward") +} +``` + +**Sheet** — on the body: +```swift +.sheet(isPresented: $showSharePicker) { + let shareChats = filterChatsToForwardTo(chats: ChatModel.shared.chats) + if #available(iOS 16.0, *) { + ChatItemForwardingView( + title: "Share channel", + chats: shareChats, + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelect: { chat in selectShareDestination(chat) } + ).presentationDetents([.fraction(0.8)]) + } else { + ChatItemForwardingView( + title: "Share channel", + chats: shareChats, + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelect: { chat in selectShareDestination(chat) } + ) + } +} +``` + +**selectShareDestination helper** in the same struct: +```swift +private func selectShareDestination(_ chat: Chat) { + showSharePicker = false + composeState = ComposeState(sharingChatCard: groupInfo) + if chat.id != ChatModel.shared.chatId { + ItemsModel.shared.loadOpenChat(chat.id) + } + dismiss() // dismiss info sheet too +} +``` + +**ChatView.swift:505-517**: pass `composeState: $composeState` to `GroupChatInfoView`. + +### 9. View rendering — `MsgContent.chat` text handling + +**On send**: text = `\n`. Link is the `strEncode groupLink` that the backend includes. If user typed nothing, text = just the link. + +**On display**: when rendering an `MCChat` message, strip the last line from `text` if it equals `chatLink`'s encoded link. This way: +- Old clients (no MCChat support) see text as-is: "hello\nhttps://simplex.chat/g#..." — usable. +- New clients (MCChat support) see "hello" + the rendered card — no redundant link. + +Implement in the card view's text rendering (§10 below). The stripping logic: +```swift +func chatCardText(_ text: String, _ chatLink: MsgChatLink) -> String { + let link = chatLinkStr(chatLink) + if text.hasSuffix("\n" + link) { + return String(text.dropLast(link.count + 1)) + } + return text +} +``` +Where `chatLinkStr` extracts the encoded link from the `MsgChatLink` variant. + +### 10. Card rendering — shared component from `CIGroupInvitationView` + +Extract a reusable **`CICardView`** from `CIGroupInvitationView`. This is a shared component that both views use (not a copy-paste). + +**`CICardView`** (new file `Shared/Views/Chat/ChatItem/CICardView.swift`): +Provides the outer frame: background with chat-tail padding, ZStack with bottomTrailing meta, VStack with: +- Header slot (profile image + name) +- Divider +- Body slot (action text, subtitle) + +Parameterized by: +```swift +struct CICardView: View { + @ObservedObject var chat: Chat + var chatItem: ChatItem + var header: Header + var body_: Body // avoid collision with View.body + var onTap: (() -> Void)? +} +``` + +**`CIGroupInvitationView`** refactored to use `CICardView`: +- Passes `groupInfoView(action)` as header +- Passes invitation text + "Tap to join" as body +- Passes `joinGroup(groupId)` as onTap +- All existing behaviour preserved (status checks, progress indicator, incognito) + +**`CIChatLinkView`** (new file, uses `CICardView`): +- Header: `ProfileImage` from `groupProfile.image` / `profile.image` + display name. Icon = same as chat list (no need to invent): + - `.group` channel (`publicGroup?.groupType == .channel`): `antenna.radiowaves.left.and.right.circle.fill` (from `GroupInfo.chatIconName` when `useRelays`) + - `.group` non-channel: `person.2.circle.fill` (from `GroupInfo.chatIconName` default) + - `.group` business: `briefcase.circle.fill` (from `GroupInfo.chatIconName` business case) + - `.contact` non-bot: `person.crop.circle.fill` (from `Contact.chatIconName`) + - `.contact` business address: `briefcase.circle.fill` + - `.invitation`: `person.crop.circle.fill` +- Body: stripped text (via `chatCardText`) + subtitle line: + - If `ownerSig != nil`: "signed" (secondary color) — same as CLI, it's a claim, verification happens on tap + - Action line: "Tap to open" (primary color) +- onTap (both sent and received): calls `planAndConnect(connLink, linkOwnerSig: ownerSig, ...)` — full connect flow with owner verification + +**Wire-in** at `ChatItemView.swift:73-90`, before the `isShortEmoji` check: +```swift +if case let .chat(_, chatLink, _) = ci.content.msgContent { + CIChatLinkView(chat: chat, chatItem: ci, chatLink: chatLink) +} else if let mc = ... { +``` + +### 11. Connect flow — owner verification in alerts + +**`planAndConnect`** (`NewChatView.swift:1218`): add optional `linkOwnerSig: LinkOwnerSig? = nil` parameter. Pass to `apiConnectPlan(connLink:linkOwnerSig:)`. + +**Alert text** — in the `.ok` branches of `planAndConnect` where the alert is built (lines 1255-1410), extend the alert body with owner verification info: +- `case .verified`: append "Channel owner signature verified." to alert message. +- `case .failed(let reason)`: append "Owner signature verification failed: \(reason)." to alert message. Consider making this a warning-styled alert. +- `nil` (no sig): no additional text. + +This surfaces in the standard connect-confirmation alert before the user taps "Connect" / "Join". + +### 12. Haskell — strip ownerSig on forward + +When a received MCChat message is forwarded (the existing forward-items path in `Library/Commands.hs`), the `dropSig` function already strips `ownerSig` for cross-chat forwarding (binding mismatch). The existing code at `Commands.hs:1000-1006` handles this. **No additional Haskell work for v1.** + +Card forwarding (subscriber shares the card further) naturally produces an unsigned card — the subscriber doesn't have the owner's key. This is correct. + +--- + +## C. Decisions — all resolved + +1. **Icon for "share" button**: `arrowshape.turn.up.forward` — easy to change later. +2. **Text stripping**: strip only the last line if it exactly matches the encoded link. User doesn't control the last line (backend appends it). If user also types the link, the typed copy remains — no special handling needed. +3. **"signed" label on card**: shown unconditionally when `ownerSig != nil`. It's a claim (same as CLI "signed"); verification happens on tap via `planAndConnect`. +4. **Card tap for sent items**: yes, same `planAndConnect` flow for both sent and received. +5. **Icons**: reuse existing `chatIconName` icons from chat list — `antenna.radiowaves.left.and.right.circle.fill` (channel), `person.2.circle.fill` (group), `briefcase.circle.fill` (business), `person.crop.circle.fill` (contact/invitation). Contact address sharing accounts for business address. + +--- + +## D. Items to lock during coding (not user decisions) + +- Exact `chatApiSendCmd` decode shape vs `chatSendCmd` / `processSendMessageCmd` for `apiShareChatMsgContent` response. +- `CICardView` exact slot API: whether to use `@ViewBuilder` closures or generic type params — decide during extraction from `CIGroupInvitationView` based on what minimises the diff. +- `planAndConnect` alert builder structure — may need a helper to format the owner-verification line, to be added inline during the `.ok` branch modifications. +- `chatLinkStr` extraction — how to get the encoded link string from `MsgChatLink` for text-stripping. Likely just `strEncode` equivalent on the `connLink` / `invLink` / `groupLink` field. diff --git a/plans/2026-04-17-kotlin-share-channel-link.md b/plans/2026-04-17-kotlin-share-channel-link.md new file mode 100644 index 0000000000..0133ca377c --- /dev/null +++ b/plans/2026-04-17-kotlin-share-channel-link.md @@ -0,0 +1,561 @@ +# Kotlin/Desktop — Share chat card (MCChat) — Implementation Plan + +Port of iOS commit `f49d98511` to Kotlin multiplatform codebase. Every section maps an iOS change to its Kotlin equivalent with file:line anchors. + +--- + +## 1. Types — `ChatModel.kt` + +### 1.1 Add `LinkOwnerSig` (new type, near line 4551 after `MsgChatLink`) + +```kotlin +@Serializable +data class LinkOwnerSig( + val ownerId: String? = null, + val chatBinding: String, + val ownerSig: String +) +``` + +iOS equivalent: `ChatTypes.swift` `LinkOwnerSig` struct. + +### 1.2 Add `ownerSig` to `MCChat` (line ~4310) + +Current: `class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent()` +Change to: `class MCChat(override val text: String, val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig? = null): MsgContent()` + +### 1.3 Add `chatLinkStr` property to `MsgContent` (near `text` property) + +```kotlin +val chatLinkStr: String? + get() = (this as? MCChat)?.chatLink?.connLinkStr +``` + +### 1.4 Update `MsgContentSerializer` (lines 4366-4496) + +In the `"chat"` case of the deserializer, add `ownerSig` field: +```kotlin +"chat" -> { + val text = json["text"]?.jsonPrimitive?.content ?: "" + val chatLink = Json.decodeFromJsonElement(json["chatLink"]!!) + val ownerSig = json["ownerSig"]?.let { Json.decodeFromJsonElement(it) } + MCChat(text, chatLink, ownerSig) +} +``` + +In the serializer, add `ownerSig` to the `MCChat` case: +```kotlin +is MCChat -> buildJsonObject { + put("type", "chat") + put("text", mc.text) + put("chatLink", Json.encodeToJsonElement(mc.chatLink)) + mc.ownerSig?.let { put("ownerSig", Json.encodeToJsonElement(it)) } +} +``` + +### 1.5 Add computed properties to `MsgChatLink` (line ~4547) + +The existing `MsgChatLink` sealed class uses `@SerialName` annotations for JSON. The Haskell side uses `taggedObjectJSON` format (`{"type": "group", ...}`). Need to verify the existing `@SerialName` produces the right format — it should, since kotlinx.serialization with `classDiscriminator = "type"` matches. + +Add after the sealed class definition: +```kotlin +sealed class MsgChatLink { + // ... existing cases ... + + val isPublicGroup: Boolean + get() = (this as? Group)?.groupProfile?.publicGroup != null + + val connLinkStr: String + get() = when (this) { + is Group -> connLink + is Contact -> connLink + is Invitation -> invLink + } + + val image: String? + get() = when (this) { + is Group -> groupProfile.image + is Contact -> profile.image + is Invitation -> profile.image + } + + val displayName: String + get() = when (this) { + is Group -> groupProfile.displayName + is Contact -> profile.displayName + is Invitation -> profile.displayName + } + + val fullName: String + get() = when (this) { + is Group -> groupProfile.fullName + is Contact -> profile.fullName + is Invitation -> profile.fullName + } + + val shortDescription: String? + get() { + val s = when (this) { + is Group -> groupProfile.shortDescr + is Contact -> profile.shortDescr + is Invitation -> profile.shortDescr + } + return s?.trim()?.ifEmpty { null } + } + + val iconRes: ImageResource // for ProfileImage icon parameter + get() = when (this) { + is Group -> when (groupProfile.publicGroup?.groupType) { + GroupType.Channel -> MR.images.ic_bigtop_updates_padded + else -> MR.images.ic_supervised_user_circle_filled + } + is Contact -> if (business) MR.images.ic_work_filled_padded else MR.images.ic_account_circle_filled + is Invitation -> MR.images.ic_account_circle_filled + } + + val smallIconRes: ImageResource // for inline icon in context/quote views + get() = when (this) { + is Group -> when (groupProfile.publicGroup?.groupType) { + GroupType.Channel -> MR.images.ic_bigtop_updates + else -> MR.images.ic_group + } + is Contact -> if (business) MR.images.ic_work else MR.images.ic_person + is Invitation -> MR.images.ic_person + } + + fun infoLine(signed: Boolean): String { + var s = when (this) { + is Group -> when (groupProfile.publicGroup?.groupType) { + GroupType.Channel -> generalGetString(MR.strings.channel_link) + else -> generalGetString(MR.strings.group_link) + } + is Contact -> if (business) generalGetString(MR.strings.business_address) else generalGetString(MR.strings.contact_address) + is Invitation -> generalGetString(MR.strings.one_time_link) + } + if (signed) { + s += " " + if (isPublicGroup) generalGetString(MR.strings.from_owner) else generalGetString(MR.strings.signed_parentheses) + } + return s + } +} +``` + +Icons resolved — see "Resolved decisions" section 1. + +### 1.6 Add `OwnerVerification` type (near ConnectionPlan, line ~6844 of SimpleXAPI.kt) + +```kotlin +@Serializable +sealed class OwnerVerification { + @Serializable @SerialName("verified") object Verified : OwnerVerification() + @Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification() +} +``` + +### 1.7 Update plan types with `ownerVerification` (SimpleXAPI.kt lines 6852-6877) + +- `InvitationLinkPlan.Ok`: add `val ownerVerification: OwnerVerification? = null` +- `ContactAddressPlan.Ok`: add `val ownerVerification: OwnerVerification? = null` +- `GroupLinkPlan.Ok`: add `val ownerVerification: OwnerVerification? = null` + +--- + +## 2. API commands — `SimpleXAPI.kt` + +### 2.1 Add `ApiShareChatMsgContent` command class (near line 3626) + +```kotlin +class ApiShareChatMsgContent( + val shareChatType: ChatType, val shareChatId: Long, + val toChatType: ChatType, val toChatId: Long, + val toScope: GroupChatScope?, val sendAsGroup: Boolean +): CC() +``` + +Add `cmdString`: +```kotlin +is ApiShareChatMsgContent -> { + val asGroup = if (sendAsGroup) "(as_group=on)" else "" + "/_share chat content ${chatRef(shareChatType, shareChatId)} ${chatRef(toChatType, toChatId, toScope)}$asGroup" +} +``` + +### 2.2 Add `CR.ChatMsgContent` response (near line 6320) + +```kotlin +@Serializable @SerialName("chatMsgContent") +class ChatMsgContent(val user: UserRef, val msgContent: MsgContent): CR() +``` + +### 2.3 Add `apiShareChatMsgContent` wrapper function (near line 1133) + +```kotlin +suspend fun apiShareChatMsgContent( + rh: Long?, shareChatType: ChatType, shareChatId: Long, + toChatType: ChatType, toChatId: Long, + toScope: GroupChatScope?, sendAsGroup: Boolean +): MsgContent? { + val r = sendCmd(rh, CC.ApiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup)) + if (r is CR.ChatMsgContent) return r.msgContent + apiErrorAlert("apiShareChatMsgContent", r) + return null +} +``` + +### 2.4 Update `apiConnectPlan` (line 1488) + +Add `linkOwnerSig: LinkOwnerSig? = null` parameter. Update the `CC.APIConnectPlan` class to include it. Update cmdString to append `sig=` when present. + +--- + +## 3. Compose state — `ComposeView.kt` + `Enums.kt` + +### 3.1 Add `SharedContent.ChatLink` to `Enums.kt` (line 13-18) + +```kotlin +data class ChatLink(val groupInfo: GroupInfo): SharedContent() +``` + +This triggers the share flow: sets `chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo)` → navigates to chat list → user picks destination. + +### 3.2 Add `ChatLinkPreview` to `ComposePreview` (`ComposeView.kt` line 57-63) + +```kotlin +@Serializable class ChatLinkPreview(val chatLink: MsgChatLink, val ownerSig: LinkOwnerSig?): ComposePreview() +``` + +### 3.3 Update `ComposeState` (`ComposeView.kt` line 103-240) + +- `sendEnabled`: add `is ComposePreview.ChatLinkPreview -> true` case +- `linkPreviewAllowed`: add `is ComposePreview.ChatLinkPreview -> false` +- `attachmentPreview`: add `is ComposePreview.ChatLinkPreview -> false` + +### 3.4 Add compose preview rendering + +In the compose area where previews are rendered, add a case for `ChatLinkPreview` that shows `ComposeChatLinkView` (new composable). + +### 3.5 Add send handling + +In the send function, add case for `ChatLinkPreview`: +```kotlin +is ComposePreview.ChatLinkPreview -> { + val linkStr = preview.chatLink.connLinkStr + val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr" + send(MsgContent.MCChat(text, preview.chatLink, preview.ownerSig), ...) +} +``` + +### 3.6 Handle `SharedContent.ChatLink` in `ComposeView.kt` (line 1431-1446, `LaunchedEffect(chatModel.sharedContent.value)`) + +When the destination chat opens with `SharedContent.ChatLink`, the `LaunchedEffect` fires. At this point: +- `chatModel.chatId.value` = destination chat ID +- `shared.groupInfo` = source group (what we're sharing) +- The current chat's `ChatInfo` provides destination type/id/scope for the API call + +```kotlin +is SharedContent.ChatLink -> { + // chat variable is available in ComposeView scope — it's the destination chat + val cInfo = chat.chatInfo + val sendAsGroup = cInfo.groupInfo?.let { it.useRelays && it.membership.memberRole >= GroupMemberRole.Owner } ?: false + withBGApi { + val mc = chatModel.controller.apiShareChatMsgContent( + chat.remoteHostId, ChatType.Group, shared.groupInfo.groupId, + cInfo.chatType, cInfo.apiId, + cInfo.groupChatScope(), sendAsGroup + ) + if (mc is MsgContent.MCChat) { + composeState.value = composeState.value.copy( + preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig) + ) + } else if (mc != null) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.error_sharing_channel), + mc.toString() + ) + } + } +} +``` + +Note: `chat` is available as a parameter in the ComposeView composable scope. `withBGApi` is needed because `apiShareChatMsgContent` is a suspend function and `LaunchedEffect` already runs in a coroutine but the API call should use the standard error handling pattern. + +### 3.7 Handle `SharedContent.ChatLink` in `ShareListView.kt` (line 33-54) + +Add filtering case in the `when (sharedContent)` block: +```kotlin +is SharedContent.ChatLink -> { + hasSimplexLink = true // chat cards ARE simplex links, prohibited by SimplexLinks group pref +} +``` + +This means in `ShareListNavLinkView` (line 44): `simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)` — groups where simplex links are disabled will show as prohibited (disabled row + alert on tap). Direct chats and local notes are unaffected (line 30-31 don't check simplex links for direct). + +### 3.8 Handle `SharedContent.ChatLink` in `ShareListNavLinkView.kt` (line 28-67) + +The existing `when (chat.chatInfo)` dispatch handles click actions per chat type. For `SharedContent.ChatLink`, the click action (line 37 `directChatAction`, line 54 `groupChatAction`) opens the destination chat. `ComposeView`'s `LaunchedEffect` (§3.6) then picks up the `SharedContent.ChatLink` and sets up the compose preview. + +No changes needed to `ShareListNavLinkView` click handlers — they already open the correct chat. The `SharedContent.ChatLink` is consumed by `ComposeView`. + +### 3.9 Handle `SharedContent.ChatLink` in `ShareListToolbar` (line 142-147) + +Add title for the share list toolbar: +```kotlin +is SharedContent.ChatLink -> stringResource(MR.strings.share_channel) +``` + +### 3.10 Handle back navigation from share list with `SharedContent.ChatLink` (line 126-133) + +When user taps back on the share list with `SharedContent.ChatLink`, should navigate back to the source chat (like Forward navigates back to `fromChatInfo.id`): +```kotlin +if (sharedContent is SharedContent.ChatLink) { + chatModel.chatId.value = sharedContent.groupInfo.id +} +``` + +--- + +## 4. New composables + +### 4.1 `ComposeChatLinkView.kt` (new file) + +Near `ComposeView.kt`. Shows ProfileImage + displayName + optional shortDescription. Cancel button. Mirrors iOS `ComposeChatLinkView`. + +### 4.2 `CIChatLinkHeader.kt` (new file) + +Near `FramedItemView.kt`. Shows profile header (image + name + fullName), shortDescription, info line, "Tap to open" + meta. Mirrors iOS `CIChatLinkHeader`. + +--- + +## 5. Message rendering — `FramedItemView.kt` + +### 5.1 Add `MCChat` case in content dispatch (line ~296-341) + +After the `MCLink` case: +```kotlin +is MsgContent.MCChat -> { + val hasText = mc.text != mc.chatLink.connLinkStr + CIChatLinkHeader(chatItem = ci, chatLink = mc.chatLink, ownerSig = mc.ownerSig, hasText = hasText) + // tap gesture → planAndConnect(mc.chatLink.connLinkStr, linkOwnerSig = mc.ownerSig) + if (hasText) { + CIMarkdownText(..., stripLink = mc.chatLink.connLinkStr) + } +} +``` + +### 5.2 Add `MCChat` case in quote dispatch (line ~142-183) + +```kotlin +is MsgContent.MCChat -> { + val prefix = buildAnnotatedString { + append(mc.chatLink.displayName) + append(if (mc.text != mc.chatLink.connLinkStr) " - " else "") + } + CIQuotedMsgView(qi, stripLink = mc.chatLink.connLinkStr, prefix = prefix) + // + small icon +} +``` + +### 5.3 Add `stripLink` parameter to text rendering + +`CIMarkdownText` / `MarkdownText` (TextItemView.kt) needs `stripLink: String? = null` parameter. Inside, strip the text and formattedText before rendering. + +Add `stripTextLink` and `stripFormattedTextLink` functions near `MarkdownText`: + +```kotlin +fun stripTextLink(text: String, link: String): String = + if (text == link) "" + else if (text.endsWith("\n$link")) text.dropLast(link.length + 1) + else text + +fun stripFormattedTextLink(ft: List?, link: String): List? { + if (ft == null || ft.isEmpty() || ft.last().text != link) return ft + val result = ft.toMutableList() + result.removeLast() + val i = result.lastIndex + if (i >= 0 && result[i].format == null && result[i].text.endsWith("\n")) { + result[i] = result[i].copy(text = result[i].text.dropLast(1)) + if (result[i].text.isEmpty()) result.removeLast() + } + return result.ifEmpty { null } +} +``` + +--- + +## 6. Chat list preview — `ChatPreviewView.kt` + +### 6.1 Add content preview for `MCChat` (line ~293-337) + +```kotlin +is MsgContent.MCChat -> { + SmallContentPreview(borderColor = if (mc.chatLink.image != null) ...) { + ProfileImage(mc.chatLink.image, mc.chatLink.iconName, size) + // onClick → planAndConnect + } +} +``` + +### 6.2 Update text preview (line ~217-290) + +For `MCChat`, show `displayName + description` instead of raw text: +```kotlin +is MsgContent.MCChat -> { + val descr = mc.chatLink.shortDescription?.let { "\n$it" } ?: "" + itemText = mc.chatLink.displayName + descr + formattedText = null +} +``` + +--- + +## 7. Context/forwarding view — `ContextItemView.kt` + +### 7.1 Add `MCChat` attachment icon (line 75-84, `fun attachment()`) + +```kotlin +is MsgContent.MCChat -> mc.chatLink.smallIconRes +``` + +This returns the small icon (e.g., `MR.images.ic_bigtop_updates` for channels). The icon is rendered inline via the existing `inlineContent` mechanism in `MessageText` (line 42-58). + +### 7.2 Add `MCChat` case in `ContextMsgPreview` or `MessageText` (line 87-89) + +For MCChat, `MessageText` needs: +1. The attachment icon (from §7.1) — rendered inline by existing mechanism +2. `prefix` with `chatLink.displayName + " - "` (or just displayName if no text) — `MarkdownText` already has `prefix: AnnotatedString?` +3. `stripLink = chatLink.connLinkStr` — strips the link from text + +Modify `ContextMsgPreview` (line 87-89) or add a special case: +```kotlin +fun ContextMsgPreview(contextItem: ChatItem, lines: Int) { + val mc = contextItem.content.msgContent + if (mc is MsgContent.MCChat) { + val hasText = contextItem.text != mc.chatLink.connLinkStr + val prefix = buildAnnotatedString { append(mc.chatLink.displayName + if (hasText) " - " else "") } + MessageText(contextItem, mc.chatLink.smallIconRes, lines, prefix = prefix, stripLink = mc.chatLink.connLinkStr) + } else { + MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines) + } +} +``` + +This requires `MessageText` to accept optional `prefix` and `stripLink` parameters and pass them to `MarkdownText`. + +--- + +## 8. Group link / info views + +### 8.1 `GroupLinkView.kt` (line ~27) + +Add parameter: `groupInfo: GroupInfo? = null`. +Add "Share via chat" button when `groupInfo?.groupProfile?.publicGroup != null`. +Button action: `chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo)` + close modals + navigate to chat list. + +### 8.2 `GroupChatInfoView.kt` + +Add "Share via chat" button in the channel link section (next to existing "Share link" button). +Button action: same as 8.1 — sets `SharedContent.ChatLink` and navigates. + +No `composeState` parameter needed (unlike iOS) — the `SharedContent` pattern handles state transfer without bindings. + +### 8.3 Channel creation (equivalent of `AddChannelView`) + +Find the Kotlin channel creation flow and pass `groupInfo` to `GroupLinkView` so "Share via chat" is available during creation. + +### 8.4 Share flow summary (no separate `shareChatLink` function needed) + +Unlike iOS which has a separate `shareChatLink` free function (due to sheet-based navigation), Kotlin's flow is: + +1. User taps "Share via chat" → `chatModel.sharedContent.value = SharedContent.ChatLink(groupInfo)` + `chatModel.chatId.value = null` (navigates to chat list showing `ShareListView`) +2. `ShareListView` shows filtered chats with `hasSimplexLink = true` prohibition +3. User picks destination → `directChatAction`/`groupChatAction` opens the chat +4. `ComposeView`'s `LaunchedEffect` fires (§3.6) → calls `apiShareChatMsgContent` → sets `ComposePreview.ChatLinkPreview` +5. User types optional text, taps Send +6. Send dispatch (§3.5) constructs `MCChat(text + link, chatLink, ownerSig)` and sends + +The API call happens in `ComposeView`'s `LaunchedEffect`, not in a separate function. Error handling: if the API fails, show alert and clear `sharedContent`. + +For the **channel creation flow** (no chat open yet): when `SharedContent.ChatLink` is consumed in `ComposeView` and the API call succeeds, the preview is set directly. No draft fallback needed — the chat IS already open at that point (the user picked it from the share list). + +--- + +## 9. Connect flow + +### 9.1 Update `planAndConnect` equivalent + +Add `linkOwnerSig: LinkOwnerSig? = null` parameter. Pass to `apiConnectPlan`. Thread `ownerVerification` from plan result to connect alerts. + +### 9.2 Add `ownerVerificationMessage` function + +```kotlin +fun ownerVerificationMessage(ov: OwnerVerification?): String? = when (ov) { + is OwnerVerification.Verified -> generalGetString(MR.strings.link_signature_verified) + is OwnerVerification.Failed -> "⚠️ " + String.format(generalGetString(MR.strings.signature_verification_failed), ov.reason) + null -> null +} +``` + +### 9.3 Update connect alerts + +Add `information: String? = null` parameter to `AlertManager.showOpenChatAlert` (`AlertManager.kt` line 271). Render as a separate `Text` below subtitle with `MaterialTheme.colors.onSurface` color (not secondary — more prominent). + +Update `showPrepareContactAlert` (ConnectPlan.kt line 572) and `showPrepareGroupAlert` (line 612) to accept and pass `ownerVerification`. Thread from `planAndConnectTask` `.Ok` cases. + +--- + +## 10. String resources + +Add to `strings.xml` (all platforms). No collisions found with existing keys: +- `chat_link_channel` = "Channel link" +- `chat_link_group` = "Group link" +- `chat_link_business_address` = "Business address" +- `chat_link_contact_address` = "Contact address" +- `chat_link_one_time` = "One-time link" +- `chat_link_from_owner` = "(from owner)" +- `chat_link_signed` = "(signed)" +- `owner_verification_passed` = "Link signature verified." +- `owner_verification_failed` = "⚠️ Signature verification failed: %s." +- `error_sharing_channel` = "Error sharing channel" +- `share_via_chat` = "Share via chat" +- `share_channel` = "Share channel" +- `tap_to_open` = "Tap to open" + +--- + +## Resolved decisions (from investigation) + +### 1. Icon resource names (verified from `ChatModel.kt` and MR/images/) +- **Channel**: `MR.images.ic_bigtop_updates_padded` (used in `GroupInfo.chatIconName` when `useRelays`) +- **Group**: `MR.images.ic_supervised_user_circle_filled` (used in `GroupInfo.chatIconName` default) +- **Business**: `MR.images.ic_work_filled_padded` (used in `GroupInfo.chatIconName` business case) +- **Contact**: `MR.images.ic_account_circle_filled` (used in `Contact.chatIconName`) +- **Small (inline text) icons**: `MR.images.ic_bigtop_updates` (channel), `MR.images.ic_group` (group), `MR.images.ic_work` (business), `MR.images.ic_person` (contact/invitation) + +### 2. MsgChatLink JSON serialization +Existing `@Serializable` sealed class with `@SerialName` annotations already produces `{"type": "group", ...}` format. No custom serializer needed (confirmed by user). Keep existing pattern. + +### 3. Forwarding/sharing picker pattern +Kotlin uses `SharedContent` + navigate to chat list, NOT a sheet picker: +- Forward: sets `chatModel.sharedContent.value = SharedContent.Forward(items, fromInfo)` + `chatModel.chatId.value = null` (returns to chat list) +- Share: add `SharedContent.ChatLink(groupInfo: GroupInfo)` case → sets `sharedContent` → user picks chat from `ShareListView` → opens chat with `ChatLinkPreview` in compose + +`ShareListView.kt` (line 44) dispatches on `SharedContent` type for filtering. Add `SharedContent.ChatLink` case there with `hasSimplexLink = true` filtering. + +### 4. Navigation after share +- `ModalManager.closeAllModalsEverywhere()` dismisses all modals +- Setting `chatModel.chatId.value = chatId` navigates to a chat +- For the share flow: `shareChatLink` calls API → on success → `ModalManager.closeAllModalsEverywhere()` → sets `composeState` preview → sets `chatModel.chatId.value = destChat.id` + +### 5. Draft mechanism (verified at `ChatModel.kt:203-204`) +Same as iOS: `chatModel.draft: MutableState` and `chatModel.draftChatId: MutableState`. Used in `ComposeView.kt:435-444` for save/restore. Same fallback pattern as iOS for the channel creation flow. + +### 6. `planAndConnect` (verified at `ConnectPlan.kt:24-48`) +Single function `suspend fun planAndConnect(rhId, shortOrFullLink, close, cleanup, filterKnownContact, filterKnownGroup)` in `ConnectPlan.kt`. Add `linkOwnerSig: LinkOwnerSig? = null` parameter. Thread to `apiConnectPlan`. Thread `ownerVerification` to alert functions. + +Alert functions: `showPrepareContactAlert` (line 572) and `showPrepareGroupAlert` (line 612) use `AlertManager.privacySensitive.showOpenChatAlert(...)` which has `subtitle: String?`. Add `information: String? = null` parameter. + +### 7. Forwarding view parameterization +No `ChatItemForwardingView` to parameterize — Kotlin uses `ShareListView` which dispatches on `SharedContent` type. Add a new `SharedContent.ChatLink` case. `ShareListView` filters chats and shows the list. When user picks a chat, `ComposeView` reads `sharedContent` and sets compose state accordingly (line 1439: `is SharedContent.Forward -> composeState.value = ...`). Add handling for `SharedContent.ChatLink`. + +### 8. String resource naming +Need to check existing strings to avoid collisions. Use `chat_link_channel`, `chat_link_group`, etc. prefix pattern to avoid collision with existing `group_link` string. diff --git a/scripts/android/copy-assets.sh b/scripts/android/copy-assets.sh new file mode 100755 index 0000000000..c76383faae --- /dev/null +++ b/scripts/android/copy-assets.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu + +# Copies generated multiplatform assets into the SimpleX assets directory. +# Called by Gradle build when simplex.assets.dir property is set. +# +# Usage: copy-assets.sh + +SRC_DIR="$1/multiplatform/resources/MR/images" +DEST_DIR="$2/MR/images" + +if [ ! -d "$SRC_DIR" ]; then + echo "Error: source assets not found: $SRC_DIR (run resize.sh first)" >&2 + exit 1 +fi + +rm -rf "$DEST_DIR" +mkdir -p "$DEST_DIR" + +cp "$SRC_DIR"/* "$DEST_DIR/" +echo "Copied multiplatform assets to $DEST_DIR" diff --git a/scripts/ios/copy-assets.sh b/scripts/ios/copy-assets.sh new file mode 100755 index 0000000000..f29ebd2464 --- /dev/null +++ b/scripts/ios/copy-assets.sh @@ -0,0 +1,50 @@ +#!/bin/sh +set -eu + +# Copies generated iOS assets into SimpleXAssets.xcassets. +# Intended to run as an Xcode Run Script build phase. +# Skips silently if SIMPLEX_ASSETS is not in SWIFT_ACTIVE_COMPILATION_CONDITIONS +# or if the source directory is not found. +# +# The source path is resolved in order: +# 1. Command-line argument +# 2. SIMPLEX_ASSETS_DIR build setting (set in Local.xcconfig) +# 3. No default — skips if neither is set +# +# Manual usage: ./scripts/copy-assets.sh path/to/assets + +# Skip if SIMPLEX_ASSETS flag is not set (unless run manually outside Xcode) +if [ -n "${SWIFT_ACTIVE_COMPILATION_CONDITIONS:-}" ]; then + case " $SWIFT_ACTIVE_COMPILATION_CONDITIONS " in + *" SIMPLEX_ASSETS "*) ;; + *) exit 0 ;; + esac +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IOS_DIR="$SCRIPT_DIR/../../apps/ios/Shared/SimpleXAssets.xcassets" + +ASSETS_ROOT="${1:-${SIMPLEX_ASSETS_DIR:-}}" +if [ -z "$ASSETS_ROOT" ]; then + echo "warning: SIMPLEX_ASSETS_DIR not set and no path argument provided" >&2 + exit 0 +fi + +SRC_DIR="$ASSETS_ROOT/ios/Assets.xcassets" + +if [ ! -d "$SRC_DIR" ]; then + echo "warning: source assets not found: $SRC_DIR (run resize.sh first)" >&2 + exit 0 +fi + +# Remove old imagesets but keep root Contents.json +find "$IOS_DIR" -name "*.imageset" -type d -exec rm -rf {} + 2>/dev/null || true + +# Copy imagesets +for imageset in "$SRC_DIR"/*.imageset; do + [ -d "$imageset" ] || continue + cp -r "$imageset" "$IOS_DIR/" + echo "Copied $(basename "$imageset")" +done + +echo "Done. Assets copied to $IOS_DIR" diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 6449ac8900..9fe726a02b 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."bc5ea42bec3a63e46b191e4150dd79957f114e01" = "0lswj7crfnwzh4g28kgxxl7g4i2a9pn03pxj7sqfqy3vs83m3bax"; + "https://github.com/simplex-chat/simplexmq.git"."858fac7f4f821a2df6fbea03a1bfbb82ea9717c5" = "1fhzynf80db7h6y2wv61fsdfd80f0blja9ljsfh405r11yg2yxvi"; "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 320e51c8af..e777c6eeba 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.0.15 +version: 6.5.0.16 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 16652f90dd..e989e520a5 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -341,6 +341,7 @@ data ChatCommand | APIGetReactionMembers {userId :: UserId, groupId :: GroupId, chatItemId :: ChatItemId, reaction :: MsgReaction} | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} | APIForwardChatItems {toChatRef :: ChatRef, sendAsGroup :: ShowGroupAsSender, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} + | APIShareChatMsgContent {shareChatRef :: ChatRef, toSendRef :: SendRef} | APIUserRead UserId | UserRead | APIChatRead {chatRef :: ChatRef} @@ -469,7 +470,7 @@ data ChatCommand | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to - | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink} -- Maybe is used to report link parsing failure as special error + | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectionLink is used to report link parsing failure as special error | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData | APIPrepareGroup UserId CreatedLinkContact DirectLink GroupShortLinkData | APIChangePreparedContactUser ContactId UserId @@ -500,6 +501,7 @@ data ChatCommand | ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text} | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} + | SharePublicGroup {shareGroupName :: GroupName, toChatName :: ChatName} | SendMessage SendName Text | SendMemberContactMessage GroupName ContactName Text | AcceptMemberContact ContactName @@ -649,6 +651,12 @@ data RelayConnectionResult = RelayConnectionResult } deriving (Show) +data AddRelayResult = AddRelayResult + { relay :: UserChatRelay, + relayError :: Maybe ChatError + } + deriving (Show) + data RelayTestStep = RTSGetLink | RTSDecodeLink @@ -719,6 +727,7 @@ data ChatResponse | CRWelcome {user :: User} | CRGroupCreated {user :: User, groupInfo :: GroupInfo} | CRPublicGroupCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]} + | CRPublicGroupCreationFailed {user :: User, addRelayResults :: [AddRelayResult]} | CRGroupRelays {user :: User, groupInfo :: GroupInfo, groupRelays :: [GroupRelay]} | CRGroupMembers {user :: User, group :: Group} | CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]} @@ -760,6 +769,7 @@ data ChatResponse | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo, msgSigned :: Bool} | CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation} + | CRChatMsgContent {user :: User, msgContent :: MsgContent} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem} -- TODO add chatItem :: AChatItem | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} @@ -1007,14 +1017,14 @@ data ConnectionPlan deriving (Show) data InvitationLinkPlan - = ILPOk {contactSLinkData_ :: Maybe ContactShortLinkData} + = ILPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification} | ILPOwnLink | ILPConnecting {contact_ :: Maybe Contact} | ILPKnown {contact :: Contact} deriving (Show) data ContactAddressPlan - = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData} + = CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification} | CAPOwnLink | CAPConnectingConfirmReconnect | CAPConnectingProhibit {contact :: Contact} @@ -1023,11 +1033,17 @@ data ContactAddressPlan deriving (Show) data GroupLinkPlan - = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData} + = GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification} | GLPOwnLink {groupInfo :: GroupInfo} | GLPConnectingConfirmReconnect | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} | GLPKnown {groupInfo :: GroupInfo} + | GLPNoRelays {groupSLinkData_ :: Maybe GroupShortLinkData} + deriving (Show) + +data OwnerVerification + = OVVerified + | OVFailed {reason :: Text} deriving (Show) type DirectLink = Bool @@ -1042,11 +1058,11 @@ data GroupShortLinkInfo = GroupShortLinkInfo connectionPlanProceed :: ConnectionPlan -> Bool connectionPlanProceed = \case CPInvitationLink ilp -> case ilp of - ILPOk _ -> True + ILPOk {} -> True ILPOwnLink -> True _ -> False CPContactAddress cap -> case cap of - CAPOk _ -> True + CAPOk {} -> True CAPOwnLink -> True CAPConnectingConfirmReconnect -> True CAPContactViaAddress _ -> True @@ -1055,6 +1071,7 @@ connectionPlanProceed = \case GLPOk {} -> True GLPOwnLink _ -> True GLPConnectingConfirmReconnect -> True + GLPNoRelays _ -> False _ -> False CPError _ -> True @@ -1631,6 +1648,8 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "OV") ''OwnerVerification) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "ILP") ''InvitationLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan) @@ -1701,6 +1720,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "TE") ''TerminalEvent) $(JQ.deriveJSON defaultJSON ''RelayConnectionResult) +$(JQ.deriveJSON defaultJSON ''AddRelayResult) + $(JQ.deriveJSON (enumJSON $ dropPrefix "RTS") ''RelayTestStep) $(JQ.deriveJSON defaultJSON ''RelayTestFailure) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index d14642befc..042280e062 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -37,6 +37,7 @@ import Data.Constraint (Dict (..)) import Data.Either (fromRight, partitionEithers, rights) import Data.Foldable (foldr') import Data.Functor (($>)) +import Data.Functor.Identity (Identity (..), runIdentity) import Data.Int (Int64) import Data.List (dropWhileEnd, find, foldl', isSuffixOf, partition, sortOn, zipWith4) import Data.List.NonEmpty (NonEmpty (..)) @@ -56,9 +57,11 @@ import qualified Data.UUID.V4 as V4 import Simplex.Chat.Library.Subscriber import Simplex.Chat.Call import Simplex.Chat.Controller +import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..)) import Simplex.Chat.Files import Simplex.Chat.Markdown import Simplex.Chat.Messages +import Simplex.Chat.Messages.Batch (encodeBatchElement) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Operators @@ -101,6 +104,7 @@ import qualified Simplex.Messaging.Crypto.ShortLink as SL import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) @@ -690,7 +694,7 @@ processChatCommand vr nm = \case gInfo@GroupInfo {groupId, membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId when (isNothing scope) $ assertUserGroupRole gInfo GRAuthor let (_, ft_) = msgContentTexts mc - if prohibitedSimplexLinks gInfo membership ft_ + if prohibitedSimplexLinks gInfo membership mc ft_ then throwCmdError ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) else do -- TODO [knocking] check chat item scope? @@ -999,7 +1003,13 @@ processChatCommand vr nm = \case CTContactConnection -> throwCmdError "not supported" where prepareMsgReq :: CChatItem c -> CM (Maybe (MsgContent, Maybe CryptoFile)) - prepareMsgReq (CChatItem _ ci) = forwardMsgContent ci $>>= forwardContent ci + prepareMsgReq (CChatItem md ci) = forwardMsgContent ci $>>= forwardContent ci . dropOwnerSig + where + dropOwnerSig = \case + mc@MCChat {text, chatLink} + | SMDSnd <- md, fromChat == toChat -> mc + | otherwise -> MCChat {text, chatLink, ownerSig = Nothing} + mc -> mc forwardCIFF :: ChatItem c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of Nothing -> ciff @@ -1067,6 +1077,41 @@ processChatCommand vr nm = \case let formattedDate = formatTime defaultTimeLocale "%Y%m%d_%H%M%S" currentDate let ext = takeExtension fileName pure $ prefix <> formattedDate <> ext + APIShareChatMsgContent (ChatRef CTGroup groupId _) toSendRef -> withUser $ \user -> do + GroupInfo {groupProfile = gp@GroupProfile {publicGroup}, membership = GroupMember {memberId, memberRole}, groupKeys} <- + withFastStore $ \db -> getGroupInfo db vr user groupId + case publicGroup of + Nothing -> throwCmdError "not a public group" + Just PublicGroupProfile {groupLink} -> do + let signingKeys = case (memberRole, groupKeys) of + (GROwner, Just gk@GroupKeys {groupRootKey = GRKPrivate _}) -> Just gk + _ -> Nothing + ownerSig <- + pure signingKeys $>>= \GroupKeys {memberPrivKey} -> + mkLinkOwnerSig memberPrivKey groupLink memberId <$$> shareChatBinding user toSendRef + let text = safeDecodeUtf8 $ strEncode groupLink + pure $ CRChatMsgContent user MCChat {text, chatLink = MCLGroup groupLink gp, ownerSig} + where + mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig + mkLinkOwnerSig privKey connLink MemberId {unMemberId} (cbTag, bindingData) = + let ownerId = Just $ B64UrlByteString unMemberId + cb = encodeChatBinding cbTag bindingData + ownerSig = C.sign' privKey $ cb <> smpEncode connLink + in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} + shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) + shareChatBinding u = \case + SRDirect contactId -> do + ct <- withFastStore $ \db -> getContact db vr u contactId + forM (contactConn ct) $ \conn -> + (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) + SRGroup toGroupId _ asGroup -> do + GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db vr u toGroupId + pure $ mkBinding m <$> publicGroup + where + mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} + | asGroup = (CBChannel, smpEncode pgId) + | otherwise = (CBGroup, smpEncode (pgId, memberId)) + APIShareChatMsgContent _ _ -> throwCmdError "sharing is only supported for public groups" APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of @@ -1281,7 +1326,7 @@ processChatCommand vr nm = \case Just smId -> void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing Nothing -> do - (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ mcSimple mc ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] APIRejectContact connReqId -> withUser $ \user -> do @@ -1919,7 +1964,7 @@ processChatCommand vr nm = \case where recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do subMode <- chatReadVar subscriptionMode - let short = isJust $ connShortLink =<< connLinkInv + let short = isJust $ connShortLink' =<< connLinkInv userLinkData_ | short = Just $ UserInvLinkData $ contactShortLinkData (userProfileDirect newUser Nothing Nothing True) Nothing | otherwise = Nothing @@ -1933,9 +1978,9 @@ processChatCommand vr nm = \case createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn deleteAgentConnectionAsync (aConnId' conn) pure conn' - APIConnectPlan userId (Just cLink) -> withUserId userId $ \user -> - uncurry (CRConnectionPlan user) <$> connectPlan user cLink - APIConnectPlan _ Nothing -> throwChatError CEInvalidConnReq + APIConnectPlan userId (Just cLink) linkOwnerSig_ -> withUserId userId $ \user -> + uncurry (CRConnectionPlan user) <$> connectPlan user cLink linkOwnerSig_ + APIConnectPlan _ Nothing _ -> throwChatError CEInvalidConnReq APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do let ContactShortLinkData {profile, message, business} = contactSLinkData welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId @@ -2026,7 +2071,7 @@ processChatCommand vr nm = \case -- create changed feature items (connecting incognito sends default preferences, instead of user preferences) lift . when incognito $ createContactChangedFeatureItems user ct ct' forM_ msgContent_ $ \mc -> do - let evt = XMsgNew $ MCSimple (extMsgContent mc Nothing) + let evt = XMsgNew $ mcSimple mc (msg, _) <- sendDirectContactMessage user ct' evt ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] @@ -2060,7 +2105,7 @@ processChatCommand vr nm = \case case gInfo of GroupInfo {preparedGroup = Nothing} -> throwCmdError "group doesn't have link to connect" GroupInfo {useRelays = BoolDef True, preparedGroup = Just PreparedGroup {connLinkToConnect}} -> do - sLnk <- case toShortLinkContact connLinkToConnect of + sLnk <- case connShortLink' connLinkToConnect of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners, relays})) <- getShortLinkConnReq nm user sLnk @@ -2176,7 +2221,7 @@ processChatCommand vr nm = \case Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do -- TODO [relays] member: /c api to support groups with relays -- TODO - possibly by going through APIPrepareGroup -> APIConnectPreparedGroup - (ccLink, plan) <- connectPlan user cLink `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e + (ccLink, plan) <- connectPlan user cLink Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e connectWithPlan user incognito ccLink plan Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do @@ -2194,7 +2239,7 @@ processChatCommand vr nm = \case toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e ConnectSimplex incognito -> withUser $ \user -> do - plan <- contactRequestPlan user adminContactReq Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing)) + plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing)) connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing @@ -2216,7 +2261,7 @@ processChatCommand vr nm = \case -- TODO [certs rcv] (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink - let ccLink'' = if isTrue userChatRelay then createdRelayLink ccLink' else ccLink' + let ccLink'' = if isTrue userChatRelay then setShortLinkType CCTRelay ccLink' else ccLink' withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode pure $ CRUserContactLinkCreated user ccLink'' CreateMyAddress -> withUser $ \User {userId} -> @@ -2288,6 +2333,19 @@ processChatCommand vr nm = \case toChatRef <- getChatRef user toChatName asGroup <- getSendAsGroup user toChatRef processChatCommand vr nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing + SharePublicGroup shareGroupName toChatName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user shareGroupName + toChatRef <- getChatRef user toChatName + sendRef <- case toChatRef of + ChatRef CTDirect ctId _ -> pure $ SRDirect ctId + ChatRef CTGroup gId scope_ -> do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + pure $ SRGroup gId scope_ (useRelays' gInfo) + _ -> throwCmdError "unsupported share target" + processChatCommand vr nm (APIShareChatMsgContent (ChatRef CTGroup groupId Nothing) sendRef) >>= \case + CRChatMsgContent _ mc -> + processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + r -> pure r SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg case sendName of @@ -2374,7 +2432,7 @@ processChatCommand vr nm = \case Right conn | directOrUsed ct -> (ct, conn) : ctConns _ -> ctConns ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json) - ctSndEvent (_, Connection {connId}) = (ConnectionId connId, Nothing, XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ctSndEvent (_, Connection {connId}) = (ConnectionId connId, Nothing, XMsgNew $ mcSimple mc) ctMsgReq :: (Contact, Connection) -> SndMessage -> ChatMsgReq ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, (vrValue msgBody, [msgId])) combineResults :: (Contact, Connection) -> Either ChatError SndMessage -> Either ChatError ([Int64], PQEncryption) -> Either ChatError (Contact, SndMessage) @@ -2421,13 +2479,29 @@ processChatCommand vr nm = \case APINewPublicGroup userId incognito relayIds groupProfile -> withUserId userId $ \user -> do (gProfile', memberId, groupKeys, setupLink) <- prepareGroupLink user gInfo <- newGroup user incognito gProfile' True memberId (Just groupKeys) (Just 1) - (gLink, groupRelays) <- setupLink gInfo `catchAllErrors` \e -> do + (gLink, results) <- setupLink gInfo `catchAllErrors` \e -> do deleteInProgressGroup user gInfo throwError e - createNewGroupItems user gInfo - pure $ CRPublicGroupCreated user gInfo gLink groupRelays + case partitionEithers (map snd results) of + ([], groupRelays) -> do + createNewGroupItems user gInfo + pure $ CRPublicGroupCreated user gInfo gLink groupRelays + (errors@(e : _), _) -> do + deleteInProgressGroup user gInfo + -- If all errors are temporary (network, timeout, host), throw to allow retry + if all isTempErr errors + then throwError e + else do + let relayResults = map toRelayResult results + toRelayResult (r, Left e) = AddRelayResult r (Just e) + toRelayResult (r, Right _) = AddRelayResult r Nothing + pure $ CRPublicGroupCreationFailed user relayResults where - prepareGroupLink :: User -> CM (GroupProfile, MemberId, GroupKeys, GroupInfo -> CM (GroupLink, [GroupRelay])) + isTempErr :: ChatError -> Bool + isTempErr = \case + ChatErrorAgent {agentError = e} -> temporaryOrHostError e + _ -> False + prepareGroupLink :: User -> CM (GroupProfile, MemberId, GroupKeys, GroupInfo -> CM (GroupLink, [(UserChatRelay, Either ChatError GroupRelay)])) prepareGroupLink user = do gVar <- asks random groupLinkId <- GroupLinkId <$> drgRandomBytes 16 @@ -2438,8 +2512,8 @@ processChatCommand vr nm = \case crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with entityId as linkEntityId (no server request) (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) - ccLink' <- createdChannelLink <$> shortenCreatedLink ccLink - sLnk <- case toShortLinkContact ccLink' of + ccLink' <- setShortLinkType CCTChannel <$> shortenCreatedLink ccLink + sLnk <- case connShortLink' ccLink' of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to create relayed group link: no short link" -- generate owner key, OwnerAuth signed by root key @@ -2456,8 +2530,8 @@ processChatCommand vr nm = \case subRole <- asks $ channelSubscriberRole . config gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId subRole subMode relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds) - groupRelays <- addRelays user gInfo sLnk relays - pure (gLink, groupRelays) + results <- addRelays user gInfo sLnk relays + pure (gLink, results) pure (groupProfile', memberId, groupKeys, setupLink) NewPublicGroup incognito relayIds gProfile -> withUser $ \User {userId} -> processChatCommand vr nm $ APINewPublicGroup userId incognito relayIds gProfile @@ -2570,7 +2644,7 @@ processChatCommand vr nm = \case void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') msg when (maxVersion (memberChatVRange m) < groupKnockingVersion) $ forM_ (memberConn m) $ \mConn -> do - let msg2 = XMsgNew $ MCSimple $ extMsgContent (MCText acceptedToGroupMessage) Nothing + let msg2 = XMsgNew $ mcSimple (MCText acceptedToGroupMessage) void $ sendDirectMemberMessage mConn msg2 groupId when (memberCategory m == GCInviteeMember) $ do introduceToRemaining vr user gInfo m {memberRole = role} @@ -2823,18 +2897,35 @@ processChatCommand vr nm = \case filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "leaveGroup" groupId $ do cancelFilesInProgress user filesInfo - (members, recipients) <- getRecipients user gInfo - msg <- sendGroupMessage' user gInfo recipients XGrpLeave + msg <- + if useRelays' gInfo && isRelay membership + then leaveChannelRelay gInfo + else leaveGroupSendMsg user gInfo (gInfo', scopeInfo) <- mkLocalGroupChatScope gInfo ci <- saveSndChatItem user (CDGroupSnd gInfo' scopeInfo) msg (CISndGroupEvent SGEUserLeft) toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci] -- TODO delete direct connections that were unused deleteGroupLinkIfExists user gInfo' -- member records are not deleted to keep history - deleteMembersConnections' user members True withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}} where + -- Relay leaving channel: create delivery job for cursor-based sending and async connection cleanup. + leaveChannelRelay gInfo = do + msg@SndMessage {msgBody, signedMsg_} <- + liftEither . runIdentity =<< lift (createSndMessages $ Identity (GroupId groupId, groupMsgSigning gInfo XGrpLeave, XGrpLeave)) + let body = encodeBatchElement signedMsg_ msgBody + withFastStore' $ \db -> do + deleteGroupDeliveryTasks db gInfo + deleteGroupDeliveryJobs db gInfo + createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) Nothing body + lift . void $ getDeliveryJobWorker True (groupId, DWSGroup) + pure msg + leaveGroupSendMsg user gInfo = do + (members, recipients) <- getRecipients user gInfo + msg <- sendGroupMessage' user gInfo recipients XGrpLeave + deleteMembersConnections' user members True + pure msg getRecipients user gInfo | useRelays' gInfo = do relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo @@ -2910,7 +3001,7 @@ processChatCommand vr nm = \case crClientData = encodeJSON $ CRDataGroup groupLinkId -- TODO [certs rcv] (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode - ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink + ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink gVar <- asks random gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo gLink @@ -3787,44 +3878,43 @@ processChatCommand vr nm = \case toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci) - addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [GroupRelay] + addRelays :: User -> GroupInfo -> ShortLinkContact -> [UserChatRelay] -> CM [(UserChatRelay, Either ChatError GroupRelay)] addRelays user gInfo@GroupInfo {membership} groupSLink relays = mapConcurrently addRelay relays where - addRelay :: UserChatRelay -> CM GroupRelay + addRelay :: UserChatRelay -> CM (UserChatRelay, Either ChatError GroupRelay) addRelay relay@UserChatRelay {address} = do - -- TODO [relays] owner: track and reuse relay profiles - -- TODO - single profile linked to relay configuration record (chat_relays) - -- TODO - update when fetching link data from relay address - (FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user address - lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case - Nothing -> throwChatError CEInvalidConnReq - Just (agentV, _) -> do - let chatV = agentToChatVersion agentV - gVar <- asks random - subMode <- chatReadVar subscriptionMode - connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff - (relayMember, conn, groupRelay) <- withFastStore $ \db -> do - relayMember <- createRelayForOwner db vr gVar user gInfo relay - groupRelay <- createGroupRelayRecord db gInfo relayMember relay - conn <- createRelayConnection db vr user (groupMemberId' relayMember) connId ConnPrepared chatV subMode - pure (relayMember, conn, groupRelay) - let GroupMember {memberRole = userRole, memberId = userMemberId} = membership - allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo - membershipProfile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership - GroupMember {memberId = relayMemberId} = relayMember - relayInv = GroupRelayInvitation { - fromMember = MemberIdRole userMemberId userRole, - fromMemberProfile = membershipProfile, - relayMemberId, - groupLink = groupSLink - } - dm <- encodeConnInfo $ XGrpRelayInv relayInv - (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode - let newConnStatus = if sqSecured then ConnSndReady else ConnJoined - withFastStore' $ \db -> do - void $ updateConnectionStatusFromTo db conn ConnPrepared newConnStatus - updateRelayStatusFromTo db groupRelay RSNew RSInvited + r <- tryAllErrors $ do + (FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user address + lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case + Nothing -> throwChatError CEInvalidConnReq + Just (agentV, _) -> do + let chatV = agentToChatVersion agentV + gVar <- asks random + subMode <- chatReadVar subscriptionMode + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff + (relayMember, conn, groupRelay) <- withFastStore $ \db -> do + relayMember <- createRelayForOwner db vr gVar user gInfo relay + groupRelay <- createGroupRelayRecord db gInfo relayMember relay + conn <- createRelayConnection db vr user (groupMemberId' relayMember) connId ConnPrepared chatV subMode + pure (relayMember, conn, groupRelay) + let GroupMember {memberRole = userRole, memberId = userMemberId} = membership + allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo + membershipProfile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + GroupMember {memberId = relayMemberId} = relayMember + relayInv = GroupRelayInvitation { + fromMember = MemberIdRole userMemberId userRole, + fromMemberProfile = membershipProfile, + relayMemberId, + groupLink = groupSLink + } + dm <- encodeConnInfo $ XGrpRelayInv relayInv + (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode + let newConnStatus = if sqSecured then ConnSndReady else ConnJoined + withFastStore' $ \db -> do + void $ updateConnectionStatusFromTo db conn ConnPrepared newConnStatus + updateRelayStatusFromTo db groupRelay RSNew RSInvited + pure (relay, r) privateGetUser :: UserId -> CM User privateGetUser userId = tryAllErrors (withStore (`getUser` userId)) >>= \case @@ -3888,28 +3978,29 @@ processChatCommand vr nm = \case pure (gId, chatSettings) _ -> throwCmdError "not supported" processChatCommand vr nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings - connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan) - connectPlan user (ACL SCMInvitation cLink) = case cLink of - CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing + connectPlan :: User -> AConnectionLink -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) + connectPlan user (ACL SCMInvitation cLink) sig_ = case cLink of + CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing Nothing CLShort l -> do let l' = serverShortLink l knownLinkPlans l' >>= \case Just r -> pure r Nothing -> do - (FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' contactSLinkData_ <- liftIO $ decodeLinkUserData cData - invitationReqAndPlan cReq (Just l') contactSLinkData_ + let ov = verifyLinkOwner rootKey [] l sig_ + invitationReqAndPlan cReq (Just l') contactSLinkData_ ov where knownLinkPlans l' = withFastStore $ \db -> do let inv cReq = ACCL SCMInvitation $ CCLink cReq (Just l') liftIO (getConnectionEntityViaShortLink db vr user l') >>= \case - Just (cReq, ent) -> pure $ Just (inv cReq, invitationEntityPlan Nothing ent) + Just (cReq, ent) -> pure $ Just (inv cReq, invitationEntityPlan Nothing Nothing ent) -- deleted contact is returned as known, as invitation link cannot be re-used too connect anyway Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db vr user l' - invitationReqAndPlan cReq sLnk_ contactSLinkData_ = do - plan <- invitationRequestPlan user cReq contactSLinkData_ `catchAllErrors` (pure . CPError) + invitationReqAndPlan cReq sLnk_ cld ov = do + plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError) pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) - connectPlan user (ACL SCMContact cLink) = case cLink of + connectPlan user (ACL SCMContact cLink) sig_ = case cLink of CLFull cReq -> do plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError) pure (ACCL SCMContact $ CCLink cReq Nothing, plan) @@ -3919,12 +4010,14 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do - (FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct')) _ -> do contactSLinkData_ <- liftIO $ decodeLinkUserData cData - plan <- contactRequestPlan user cReq contactSLinkData_ + let ContactLinkData _ UserContactData {owners} = cData + ov = verifyLinkOwner rootKey owners l' sig_ + plan <- contactRequestPlan user cReq contactSLinkData_ ov pure (con cReq, plan) where knownLinkPlans = withFastStore $ \db -> @@ -3945,20 +4038,24 @@ processChatCommand vr nm = \case knownLinkPlans >>= \case Just r -> pure r Nothing -> do - (fd, cData@(ContactLinkData _ UserContactData {direct, relays})) <- getShortLinkConnReq nm user l' - let FixedLinkData {linkConnReq = cReq, linkEntityId} = fd - linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} + (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData - -- Cross-validate linkEntityId and publicGroupId from profile: - -- for channels both must be present and match, for p2p groups both must be absent - let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> - fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup - case (B64UrlByteString <$> linkEntityId, profilePGId) of - (Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure () - (Nothing, Nothing) -> pure () - _ -> throwChatError CEInvalidConnReq - plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ - pure (con cReq, plan) + if not direct && null relays + then pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) + else do + let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd + linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} + -- Cross-validate linkEntityId and publicGroupId from profile: + -- for channels both must be present and match, for p2p groups both must be absent + let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> + fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup + case (B64UrlByteString <$> linkEntityId, profilePGId) of + (Just entityId, Just publicGroupId) | entityId == publicGroupId -> pure () + (Nothing, Nothing) -> pure () + _ -> throwChatError CEInvalidConnReq + let ov = verifyLinkOwner rootKey owners l' sig_ + plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov + pure (con cReq, plan) where knownLinkPlans = withFastStore $ \db -> liftIO (getGroupInfoViaUserShortLink db vr user l') >>= \case @@ -3973,9 +4070,9 @@ processChatCommand vr nm = \case processChatCommand vr nm $ APIConnectContactViaAddress userId incognito contactId _ -> processChatCommand vr nm $ APIConnect userId incognito $ Just ccLink | otherwise = pure $ CRConnectionPlan user ccLink plan - invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> CM ConnectionPlan - invitationRequestPlan user cReq contactSLinkData_ = do - maybe (CPInvitationLink (ILPOk contactSLinkData_)) (invitationEntityPlan contactSLinkData_) + invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + invitationRequestPlan user cReq cld ov = do + maybe (CPInvitationLink (ILPOk cld ov)) (invitationEntityPlan cld ov) <$> withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ invCReqSchemas cReq) where invCReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation) @@ -3983,15 +4080,15 @@ processChatCommand vr nm = \case ( CRInvitationUri crData {crScheme = SSSimplex} e2e, CRInvitationUri crData {crScheme = simplexChat} e2e ) - invitationEntityPlan :: Maybe ContactShortLinkData -> ConnectionEntity -> ConnectionPlan - invitationEntityPlan contactSLinkData_ = \case + invitationEntityPlan :: Maybe ContactShortLinkData -> Maybe OwnerVerification -> ConnectionEntity -> ConnectionPlan + invitationEntityPlan cld ov = \case RcvDirectMsgConnection Connection {connStatus, contactConnInitiated} ct_ -> case ct_ of Just ct | contactActive ct -> CPInvitationLink (ILPKnown ct) - | otherwise -> CPInvitationLink (ILPOk contactSLinkData_) + | otherwise -> CPInvitationLink (ILPOk cld ov) Nothing | connStatus == ConnNew && contactConnInitiated -> CPInvitationLink ILPOwnLink - | connStatus == ConnPrepared -> CPInvitationLink (ILPOk contactSLinkData_) + | connStatus == ConnPrepared -> CPInvitationLink (ILPOk cld ov) | otherwise -> CPInvitationLink (ILPConnecting Nothing) _ -> CPError $ ChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" contactOrGroupRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan @@ -3999,10 +4096,10 @@ processChatCommand vr nm = \case let ConnReqUriData {crClientData} = crData groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli case groupLinkId of - Nothing -> contactRequestPlan user cReq Nothing - Just _ -> groupJoinRequestPlan user cReq Nothing Nothing - contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> CM ConnectionPlan - contactRequestPlan user (CRContactUri crData) contactSLinkData_ = do + Nothing -> contactRequestPlan user cReq Nothing Nothing + Just _ -> groupJoinRequestPlan user cReq Nothing Nothing Nothing + contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + contactRequestPlan user (CRContactUri crData) cld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case @@ -4012,19 +4109,19 @@ processChatCommand vr nm = \case Nothing -> withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct) - _ -> pure $ CPContactAddress (CAPOk contactSLinkData_) + _ -> pure $ CPContactAddress (CAPOk cld ov) Just (RcvDirectMsgConnection Connection {connStatus} Nothing) - | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk contactSLinkData_) + | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov) | otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect Just (RcvDirectMsgConnection _ (Just ct)) | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) - | contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_) + | contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov) | otherwise -> pure $ CPContactAddress (CAPKnown ct) -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) - Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" - groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupJoinRequestPlan user (CRContactUri crData) groupSLinkInfo_ groupSLinkData_ = do + groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + groupJoinRequestPlan user (CRContactUri crData) linkInfo gld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case @@ -4033,21 +4130,21 @@ processChatCommand vr nm = \case connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) + (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov) -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | otherwise -> pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) + | otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov) (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo, _) -> groupPlan gInfo groupSLinkInfo_ groupSLinkData_ - groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan - groupPlan gInfo@GroupInfo {membership} groupSLinkInfo_ groupSLinkData_ + (Just gInfo, _) -> groupPlan gInfo linkInfo gld ov + groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan + groupPlan gInfo@GroupInfo {membership} linkInfo gld ov | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo) | not (memberActive membership) && not (memberRemoved membership) = pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) - | otherwise = pure $ CPGroupLink (GLPOk groupSLinkInfo_ groupSLinkData_) + | otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov) contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, @@ -4059,6 +4156,16 @@ processChatCommand vr nm = \case serverShortLink = \case CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey + verifyLinkOwner :: ConnectionModeI m => C.PublicKeyEd25519 -> [OwnerAuth] -> ConnShortLink m -> Maybe LinkOwnerSig -> Maybe OwnerVerification + verifyLinkOwner rootKey owners connLink = + fmap $ \LinkOwnerSig {ownerId, chatBinding = B64UrlByteString bindingBytes, ownerSig} -> + let signedData = bindingBytes <> smpEncode connLink + findOwner (B64UrlByteString oId) = find (\OwnerAuth {ownerId = oId'} -> oId' == oId) owners + in case maybe (Just rootKey) (fmap ownerKey . findOwner) ownerId of + Nothing -> OVFailed "unknown owner" + Just key + | C.verify' key ownerSig signedData -> OVVerified + | otherwise -> OVFailed "signature verification failed" contactShortLinkData :: Profile -> Maybe AddressSettings -> UserLinkData contactShortLinkData p settings = let msg = autoReply =<< settings @@ -4070,7 +4177,7 @@ processChatCommand vr nm = \case encodeShortLinkData $ RelayAddressLinkData {relayProfile = RelayProfile {displayName, fullName, shortDescr, image}} updatePCCShortLinkData :: PendingContactConnection -> Profile -> CM (Maybe ShortLinkInvitation) updatePCCShortLinkData conn@PendingContactConnection {connLinkInv} profile = - forM (connShortLink =<< connLinkInv) $ \_ -> do + forM (connShortLink' =<< connLinkInv) $ \_ -> do let userData = contactShortLinkData profile Nothing userLinkData = UserInvLinkData userData shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing) @@ -4134,9 +4241,9 @@ processChatCommand vr nm = \case prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (MsgContainer, Maybe (CIQuote 'CTDirect))) prepareMsgs cmsFileInvs timed_ = withFastStore $ \db -> forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded, _, _), fInv_) -> do - case (quotedItemId, itemForwarded) of - (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing Nothing), Nothing) - (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing Nothing), Nothing) + (mc', quotedItem_) <- case (quotedItemId, itemForwarded) of + (Nothing, Nothing) -> pure (mcSimple mc, Nothing) + (Nothing, Just _) -> pure (mcForward mc, Nothing) (Just qiId, Nothing) -> do CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- getDirectChatItem db user contactId qiId @@ -4144,8 +4251,9 @@ processChatCommand vr nm = \case let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} qmc = quoteContent mc origQmc file quotedItem = CIQuote {chatDir = qd, itemId = Just qiId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} - pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing Nothing), Just quotedItem) + pure (mcQuote QuotedMsg {msgRef, content = qmc} mc, Just quotedItem) (Just _, Just _) -> throwError SEInvalidQuote + pure (mc' {file = fInv_, ttl = ttl' <$> timed_, live = justTrue live}, quotedItem_) where quoteData :: ChatItem c d -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTDirect, Bool) quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwError SEInvalidQuote @@ -4771,6 +4879,7 @@ chatCommandP = "/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> (knownReaction <$?> jsonP)), "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), "/_forward " *> (APIForwardChatItems <$> chatRefP <*> (" as_group=" *> onOffP <|> pure False) <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), + "/_share chat content " *> (APIShareChatMsgContent <$> chatRefP <* A.space <*> sendRefP), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, "/_read chat " *> (APIChatRead <$> chatRefP), @@ -4942,7 +5051,7 @@ chatCommandP = (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP), "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, - "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing)), + "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> optional (" sig=" *> jsonP)), "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <* A.space <*> jsonP), "/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal), @@ -4959,6 +5068,7 @@ chatCommandP = ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, + "/share chat #" *> (SharePublicGroup <$> displayNameP <* A.space <*> chatNameP), SendMessage <$> sendNameP <* A.space <*> msgTextP, "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), "/accept_member_contact @" *> (AcceptMemberContact <$> displayNameP), diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 4ff677f379..f82b6884b2 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -202,24 +202,22 @@ toggleNtf m ntfOn = withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchAllErrors` eToView prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> Maybe MsgScope -> ShowGroupAsSender -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)) -prepareGroupMsg db user g@GroupInfo {membership} msgScope showGroupAsSender mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of - (Nothing, Nothing) -> - let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope (justTrue showGroupAsSender) - in pure (XMsgNew mc', Nothing) - (Nothing, Just _) -> - let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope (justTrue showGroupAsSender) - in pure (XMsgNew mc', Nothing) - (Just quotedItemId, Nothing) -> do - CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, mentions = quoteMentions, file} <- - getGroupCIWithReactions db user g quotedItemId - (origQmc, qd, sent, member_) <- quoteData qci membership - let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = memberId' <$> member_} - qmc = quoteContent mc origQmc file - (qmc', ft', _) = updatedMentionNames qmc formattedText quoteMentions - quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc', formattedText = ft'} - mc' = MCQuote QuotedMsg {msgRef, content = qmc'} (ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope (justTrue showGroupAsSender)) - pure (XMsgNew mc', Just quotedItem) - (Just _, Just _) -> throwError SEInvalidQuote +prepareGroupMsg db user g@GroupInfo {membership} msgScope showGroupAsSender mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = do + (mc', quotedItem_) <- case (quotedItemId_, itemForwarded) of + (Nothing, Nothing) -> pure (mcSimple mc, Nothing) + (Nothing, Just _) -> pure (mcForward mc, Nothing) + (Just quotedItemId, Nothing) -> do + CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, mentions = quoteMentions, file} <- + getGroupCIWithReactions db user g quotedItemId + (origQmc, qd, sent, member_) <- quoteData qci membership + let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = memberId' <$> member_} + qmc = quoteContent mc origQmc file + (qmc', ft', _) = updatedMentionNames qmc formattedText quoteMentions + quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc', formattedText = ft'} + pure (mcQuote QuotedMsg {msgRef, content = qmc'} mc, Just quotedItem) + (Just _, Just _) -> throwError SEInvalidQuote + let mc'' = mc' {mentions = MsgMentions mentions, file = fInv_, ttl = ttl' <$> timed_, live = justTrue live, scope = msgScope, asGroup = justTrue showGroupAsSender} + pure (XMsgNew mc'', quotedItem_) where quoteData :: ChatItem c d -> GroupMember -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTGroup, Bool, Maybe GroupMember) quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwError SEInvalidQuote @@ -343,7 +341,7 @@ prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) && not hostApprovalVoice = Just GFVoice | isNothing scopeInfo && not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles | isNothing scopeInfo && isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports - | isNothing scopeInfo && prohibitedSimplexLinks gInfo m ft = Just GFSimplexLinks + | isNothing scopeInfo && prohibitedSimplexLinks gInfo m mc ft = Just GFSimplexLinks | otherwise = Nothing where hostApprovalVoice @@ -360,10 +358,14 @@ prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole | sent = userRole >= GRModerator | otherwise = userRole < GRModerator -prohibitedSimplexLinks :: GroupInfo -> GroupMember -> Maybe MarkdownList -> Bool -prohibitedSimplexLinks gInfo m ft = +prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Bool +prohibitedSimplexLinks gInfo m mc ft = not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) - && maybe False (any ftIsSimplexLink) ft + && (isChatLink mc || maybe False (any ftIsSimplexLink) ft) + where + isChatLink = \case + MCChat {} -> True + _ -> False ftIsSimplexLink :: FormattedText -> Bool ftIsSimplexLink FormattedText {format} = maybe False isSimplexLink format @@ -1062,7 +1064,7 @@ introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRol let msg = if maxVersion (memberChatVRange m) >= groupKnockingVersion then XGrpLinkAcpt GAPendingReview memberRole memberId - else XMsgNew $ MCSimple $ extMsgContent (MCText pendingReviewMessage) Nothing + else XMsgNew $ mcSimple (MCText pendingReviewMessage) void $ sendDirectMemberMessage mConn msg groupId modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo let rcpModMs = filter shouldIntroduceToMod modMs @@ -1200,7 +1202,7 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c | useRelays' gInfo = Nothing | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do let GroupInfo {groupProfile = GroupProfile {description}} = gInfo - fmap (\descr -> XMsgNew $ MCSimple $ extMsgContent (MCText descr) Nothing) description + fmap (\descr -> XMsgNew $ mcSimple (MCText descr)) description | otherwise = Nothing itemForwardEvents :: CChatItem 'CTGroup -> CM [ChatMsgEvent 'Json] itemForwardEvents cci = case cci of @@ -1302,8 +1304,8 @@ setGroupLinkData nm user gInfo gLink = do (conn, groupRelays) <- withFastStore $ \db -> (,) <$> getGroupLinkConnection db vr user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays - tagShortLink = if useRelays' gInfo then toShortChannelLink else toShortGroupLink - sLnk <- shortenShortLink' . tagShortLink =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) + linkType = if useRelays' gInfo then CCTChannel else CCTGroup + sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () @@ -1328,27 +1330,38 @@ updatePublicGroupData user gInfo -- TODO [relays] owner: set owners on updating link data (multi-owner) groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData) -groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}} GroupLink {groupLinkId} groupRelays = +groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}, membership = GroupMember {memberId}, groupKeys} GroupLink {groupLinkId} groupRelays = let direct = not $ useRelays' gInfo relays = mapMaybe (\GroupRelay {relayLink} -> relayLink) groupRelays publicGroupData_ = PublicGroupData <$> publicMemberCount userData = encodeShortLinkData $ GroupShortLinkData {groupProfile, publicGroupData = publicGroupData_} - userLinkData = UserContactLinkData UserContactData {direct, owners = [], relays, userData} + owners = case groupKeys of + Just GroupKeys {groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} -> + let ownerId = unMemberId memberId + ownerKey = C.publicKey memberPrivKey + authOwnerSig = C.sign' rootPrivKey (ownerId <> C.encodePubKey ownerKey) + in [OwnerAuth {ownerId, ownerKey, authOwnerSig}] + _ -> [] + userLinkData = UserContactLinkData UserContactData {direct, owners, relays, userData} crClientData = encodeJSON $ CRDataGroup groupLinkId in (userLinkData, crClientData) restoreShortLink' :: ConnShortLink m -> CM (ConnShortLink m) restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) -getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) -getShortLinkConnReq nm user@User {userChatRelay} l = do +getShortLinkConnReq' :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) +getShortLinkConnReq' nm user l = do l' <- restoreShortLink' l - (fd, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' + withAgent $ \a -> getConnShortLink a nm (aUserId user) l' + +getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) +getShortLinkConnReq nm user l = do + (fd, cData) <- getShortLinkConnReq' nm user l case cData of ContactLinkData _ UserContactData {direct, relays} | not supported -> throwChatError CEUnsupportedConnReq where - supported = direct || not (null relays) || isTrue userChatRelay + supported = direct || not (null relays) _ -> pure () pure (fd, cData) @@ -1382,27 +1395,6 @@ shortenShortLink' l = (`shortenShortLink` l) <$> asks (shortLinkPresetServers . shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m) shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM shortenShortLink' sLnk -createdGroupLink :: CreatedLinkContact -> CreatedLinkContact -createdGroupLink (CCLink cReq shortLink) = CCLink cReq (toShortGroupLink <$> shortLink) - -toShortGroupLink :: ShortLinkContact -> ShortLinkContact -toShortGroupLink (CSLContact sch _ srv k) = CSLContact sch CCTGroup srv k - -createdChannelLink :: CreatedLinkContact -> CreatedLinkContact -createdChannelLink (CCLink cReq shortLink) = CCLink cReq (toShortChannelLink <$> shortLink) - -toShortChannelLink :: ShortLinkContact -> ShortLinkContact -toShortChannelLink (CSLContact sch _ srv k) = CSLContact sch CCTChannel srv k - -createdRelayLink :: CreatedLinkContact -> CreatedLinkContact -createdRelayLink (CCLink cReq shortLink) = CCLink cReq (toShortRelayLink <$> shortLink) - -toShortRelayLink :: ShortLinkContact -> ShortLinkContact -toShortRelayLink (CSLContact sch _ srv k) = CSLContact sch CCTRelay srv k - -toShortLinkContact :: CreatedLinkContact -> Maybe ShortLinkContact -toShortLinkContact (CCLink _cReq sLink) = sLink - deleteGroupLink' :: User -> GroupInfo -> CM () deleteGroupLink' user gInfo = do vr <- chatVersionRange diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index f5f7752ca4..e92622b60c 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -700,7 +700,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just UserContactRequest {welcomeSharedMsgId = Just smId} -> void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing _ -> do - (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ mcSimple mc ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] @@ -987,7 +987,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = checkSendAsGroup asGroup $ memberCanSend (Just m'') scope $ newGroupContentMessage gInfo' (Just m'') mc msg brokerTs False where - ExtMsgContent {scope, asGroup} = mcExtMsgContent mc + MsgContainer {scope, asGroup} = mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo' (Just m'') sharedMsgId fileDescr XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ -> @@ -1217,7 +1217,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just UserContactRequest {welcomeSharedMsgId = Just smId} -> void $ sendGroupMessage' user gInfo [m] $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing _ -> do - msg <- sendGroupMessage' user gInfo [m] $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + msg <- sendGroupMessage' user gInfo [m] $ XMsgNew $ mcSimple mc ci <- saveSndChatItem user (CDGroupSnd gInfo Nothing) msg (CISndMsgContent mc) withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing) ci] @@ -1526,7 +1526,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = muteEventInChannel :: GroupInfo -> GroupMember -> Bool muteEventInChannel gInfo@GroupInfo {membership} m = - useRelays' gInfo && memberRole' membership < GRModerator && not (isRelay membership) && memberRole' m < GRModerator + useRelays' gInfo + && not (isRelay membership) -- relay users see all events + && not (isRelay m) -- relay events (e.g. leave) are visible to all + && memberRole' membership < GRModerator + && memberRole' m < GRModerator memberCanSend :: Maybe GroupMember -> Maybe MsgScope -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext) memberCanSend Nothing _ a = a -- channel message - was previously checked and allowed by relay @@ -1726,7 +1730,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> CM () newContentMessage ct mc msg@RcvMessage {sharedMsgId_} msgMeta = do - let ExtMsgContent content _ fInv_ _ _ _ _ = mcExtMsgContent mc + let MsgContainer {content = c, file = fInv_} = mc + content <- case c of + MCChat {text, chatLink, ownerSig = Just LinkOwnerSig {chatBinding = B64UrlByteString binding}} -> do + keepSig <- case contactConn ct of + Nothing -> pure False + Just conn -> do + adHash <- withAgent (`getConnectionRatchetAdHash` aConnId conn) + pure $ encodeChatBinding CBDirect adHash == binding + pure $ if keepSig then c else MCChat {text, chatLink, ownerSig = Nothing} + _ -> pure c -- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete -- case content of -- MCText "hello 111" -> @@ -1737,7 +1750,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do void $ newChatItem (ciContentNoParse $ CIRcvChatFeatureRejected CFVoice) Nothing Nothing False else do - let ExtMsgContent _ _ _ itemTTL live_ _ _ = mcExtMsgContent mc + let MsgContainer {ttl = itemTTL, live = live_} = mc timed_ = rcvContactCITimed ct itemTTL live = fromMaybe False live_ file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct @@ -1979,7 +1992,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = rejected gInfo' m' scopeInfo f = newChatItem gInfo' m' scopeInfo (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False timed_ gInfo' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo' itemTTL live' = fromMaybe False live_ - ExtMsgContent content mentions fInv_ itemTTL live_ msgScope_ asGroup_ = mcExtMsgContent mc + MsgContainer {content = c, mentions = MsgMentions mentions, file = fInv_, ttl = itemTTL, live = live_, scope = msgScope_, asGroup = asGroup_} = mc + content = case c of + MCChat {text, chatLink, ownerSig = Just LinkOwnerSig {chatBinding = B64UrlByteString binding}} -> case publicGroup of + Just pgp | maybe False (binding ==) (expectedBinding pgp) -> c + _ -> MCChat {text, chatLink, ownerSig = Nothing} + _ -> c + expectedBinding PublicGroupProfile {publicGroupId} + | sentAsGroup = Just $ encodeChatBinding CBChannel (smpEncode publicGroupId) + | otherwise = (\GroupMember {memberId} -> encodeChatBinding CBGroup (smpEncode (publicGroupId, memberId))) <$> m_ + GroupInfo {groupProfile = GroupProfile {publicGroup}} = gInfo sentAsGroup = asGroup_ == Just True ts@(_, ft_) = msgContentTexts content -- m' is Maybe GroupMember @@ -2021,7 +2043,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let fileMember_ = if sentAsGroup then Nothing else m' in processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ newChatItem gInfo' m' scopeInfo ciContent ciFile_ timed live = do - let mentions' = if maybe False memberBlocked m' then [] else mentions + let mentions' = if maybe False memberBlocked m' then M.empty else mentions (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo ciContent ciFile_ timed live mentions' ci' <- maybe (pure ci) (\m -> blockedMemberCI gInfo' m ci) m' let memberId_ = memberId' <$> m' @@ -2030,7 +2052,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMessageUpdate :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> Maybe MsgScope -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> Maybe Bool -> CM (Maybe DeliveryTaskContext) groupMessageUpdate gInfo@GroupInfo {groupId} m_ sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId} brokerTs ttl_ live_ asGroup_ - | Just m <- m_, prohibitedSimplexLinks gInfo m ft_ = + | Just m <- m_, prohibitedSimplexLinks gInfo m mc ft_ = messageWarning ("x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks) $> Nothing | otherwise = do updateRcvChatItem `catchCINotFound` \_ -> do @@ -2900,8 +2922,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise -> messageError "x.grp.mem.intro ignored: member already exists" Left _ - | useRelays' gInfo -> - void $ withStore $ \db -> createIntroReMember db user gInfo memInfo memRestrictions + | useRelays' gInfo -> do + -- owner key must only come from link data, not from relay intro + let memInfo' = case memInfo of + MemberInfo mId mRole v p _ + | mRole == GROwner -> MemberInfo mId mRole v p Nothing + _ -> memInfo + void $ withStore $ \db -> createIntroReMember db user gInfo memInfo' memRestrictions | otherwise -> do when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) case memChatVRange of @@ -3115,6 +3142,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = deleteMemberConnection m -- member record is not deleted to allow creation of "member left" chat item gInfo' <- updateMemberRecordDeleted user gInfo m GSMemLeft + when (isRelay m) $ + withStore' $ \db -> do + relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m) + forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive gInfo'' <- updatePublicGroupData user gInfo' unless (muteEventInChannel gInfo'' m) $ do (gInfo''', m', scopeInfo) <- mkGroupChatScope gInfo'' m @@ -3285,7 +3316,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XMsgNew mc -> void $ memberCanSend author_ scope $ newGroupContentMessage gInfo author_ mc rcvMsg msgTs True where - ExtMsgContent {scope} = mcExtMsgContent mc + MsgContainer {scope} = mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> void $ groupMessageFileDescription gInfo author_ sharedMsgId fileDescr XMsgUpdate sharedMsgId mContent mentions ttl live msgScope asGroup_ -> @@ -3693,7 +3724,7 @@ runRelayRequestWorker a Worker {doWork} = do where getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact) getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} gInfo = do - (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq NRMBackground user reqGroupLink + (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" Just GroupShortLinkData {groupProfile = gp@GroupProfile {publicGroup}} -> do @@ -3724,8 +3755,8 @@ runRelayRequestWorker a Worker {doWork} = do let crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with relayMemId as linkEntityId (no server request) (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) - ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink - sLnk <- case toShortLinkContact ccLink' of + ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink + sLnk <- case connShortLink' ccLink' of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to create relay link: no short link" let userData = encodeShortLinkData $ RelayShortLinkData {relayProfile = fromLocalProfile p} diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index e404388d8d..d90429f58e 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -1308,11 +1308,6 @@ data CIForwardedFrom | 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_ diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 3bbfb02d0a..03cc38e12a 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -60,10 +60,10 @@ import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) usageConditionsCommit :: Text -usageConditionsCommit = "7471fd2af5838dc0467aebc570b5ea75e5df3209" +usageConditionsCommit = "05f99634c470f8bddac20046947a0606938b22ad" previousConditionsCommit :: Text -previousConditionsCommit = "a5061f3147165a05979d6ace33960aced2d6ac03" +previousConditionsCommit = "7471fd2af5838dc0467aebc570b5ea75e5df3209" usageConditionsText :: Text usageConditionsText = diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 3485849741..b7e838c52b 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -318,7 +318,7 @@ data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMess data KeyRef = KRMember deriving (Eq, Show) -data ChatBinding = CBGroup +data ChatBinding = CBGroup | CBDirect | CBChannel deriving (Eq, Show) data MsgSignature = MsgSignature KeyRef C.ASignature @@ -381,10 +381,15 @@ instance Encoding KeyRef where c -> fail $ "invalid KeyRef tag: " <> show c instance Encoding ChatBinding where - smpEncode CBGroup = "G" + smpEncode = \case + CBGroup -> "G" + CBDirect -> "D" + CBChannel -> "C" smpP = A.anyChar >>= \case 'G' -> pure CBGroup + 'D' -> pure CBDirect + 'C' -> pure CBChannel c -> fail $ "invalid ChatBinding: " <> show c instance ToField ChatBinding where toField = toField . decodeLatin1 . smpEncode @@ -411,7 +416,8 @@ data MsgSigning = MsgSigning privKey :: C.PrivateKeyEd25519 } - +encodeChatBinding :: ChatBinding -> ByteString -> ByteString +encodeChatBinding cb bindingData = smpEncode cb <> bindingData data ChatMsgEvent (e :: MsgEncoding) where XMsgNew :: MsgContainer -> ChatMsgEvent 'Json @@ -477,8 +483,8 @@ deriving instance Show AChatMsgEvent -- actual filtering on forwarding is done in processEvent isForwardedGroupMsg :: ChatMsgEvent e -> Bool isForwardedGroupMsg ev = case ev of - XMsgNew mc -> case mcExtMsgContent mc of - ExtMsgContent {file = Just FileInvitation {fileInline = Just _}} -> False + XMsgNew mc -> case mc of + MsgContainer {file = Just FileInvitation {fileInline = Just _}} -> False _ -> True XMsgFileDescr _ _ -> True XMsgUpdate {} -> True @@ -582,7 +588,7 @@ data QuotedMsg = QuotedMsg {msgRef :: MsgRef, content :: MsgContent} cmToQuotedMsg :: AChatMsgEvent -> Maybe QuotedMsg cmToQuotedMsg = \case - ACME _ (XMsgNew (MCQuote quotedMsg _)) -> Just quotedMsg + ACME _ (XMsgNew MsgContainer {quote = Just quotedMsg}) -> Just quotedMsg _ -> Nothing data MsgContentTag @@ -631,24 +637,51 @@ instance FromField MsgContentTag where fromField = fromTextField_ $ eitherToMayb instance ToField MsgContentTag where toField = toField . safeDecodeUtf8 . strEncode -data MsgContainer - = MCSimple ExtMsgContent - | MCQuote QuotedMsg ExtMsgContent - | MCComment MsgRef ExtMsgContent - | MCForward ExtMsgContent +-- Wire JSON 1:1 with parsed form. The three discriminator fields `quote`, `parent`, +-- and `forward` are independent and may co-occur (e.g. a comment that quotes another +-- comment carries both `parent` and `quote`). `forward` is `Maybe Bool` for backwards +-- compatibility with the previous wire encoding: the serializer omits the field when +-- `Nothing` and the parser treats absent/false as "not a forward". +data MsgContainer = MsgContainer + { content :: MsgContent, + -- the key used in mentions is a locally (per message) unique display name of member. + -- Suffixes _1, _2 should be appended to make names locally unique. + -- It should be done in the UI, as they will be part of the text, and validated in the API. + mentions :: MsgMentions, + file :: Maybe FileInvitation, + ttl :: Maybe Int, + live :: Maybe Bool, + scope :: Maybe MsgScope, + asGroup :: Maybe Bool, + quote :: Maybe QuotedMsg, + parent :: Maybe MsgRef, + forward :: Maybe Bool + } deriving (Eq, Show) -mcExtMsgContent :: MsgContainer -> ExtMsgContent -mcExtMsgContent = \case - MCSimple c -> c - MCQuote _ c -> c - MCComment _ c -> c - MCForward c -> c +mcSimple :: MsgContent -> MsgContainer +mcSimple content = + MsgContainer + { content, + mentions = MsgMentions M.empty, + file = Nothing, + ttl = Nothing, + live = Nothing, + scope = Nothing, + asGroup = Nothing, + quote = Nothing, + parent = Nothing, + forward = Nothing + } -isMCForward :: MsgContainer -> Bool -isMCForward = \case - MCForward _ -> True - _ -> False +mcQuote :: QuotedMsg -> MsgContent -> MsgContainer +mcQuote q c = (mcSimple c) {quote = Just q} + +mcComment :: MsgRef -> MsgContent -> MsgContainer +mcComment p c = (mcSimple c) {parent = Just p} + +mcForward :: MsgContent -> MsgContainer +mcForward c = (mcSimple c) {forward = Just True} data MsgContent = MCText {text :: Text} @@ -658,7 +691,7 @@ data MsgContent | MCVoice {text :: Text, duration :: Int} | MCFile {text :: Text} | MCReport {text :: Text, reason :: ReportReason} - | MCChat {text :: Text, chatLink :: MsgChatLink} + | MCChat {text :: Text, chatLink :: MsgChatLink, ownerSig :: Maybe LinkOwnerSig} | MCUnknown {tag :: Text, text :: Text, json :: J.Object} deriving (Eq, Show) @@ -668,6 +701,13 @@ data MsgChatLink | MCLGroup {connLink :: ShortLinkContact, groupProfile :: GroupProfile} deriving (Eq, Show) +data LinkOwnerSig = LinkOwnerSig + { ownerId :: Maybe B64UrlByteString, + chatBinding :: B64UrlByteString, + ownerSig :: C.Signature 'C.Ed25519 + } + deriving (Eq, Show) + msgContentText :: MsgContent -> Text msgContentText = \case MCText t -> t @@ -722,29 +762,100 @@ msgContentTag = \case MCChat {} -> MCChat_ MCUnknown {tag} -> MCUnknown_ tag -data ExtMsgContent = ExtMsgContent - { content :: MsgContent, - -- the key used in mentions is a locally (per message) unique display name of member. - -- Suffixes _1, _2 should be appended to make names locally unique. - -- It should be done in the UI, as they will be part of the text, and validated in the API. - mentions :: Map MemberName MsgMention, - file :: Maybe FileInvitation, - ttl :: Maybe Int, - live :: Maybe Bool, - scope :: Maybe MsgScope, - asGroup :: Maybe Bool - } +data MsgMention = MsgMention {memberId :: MemberId} deriving (Eq, Show) -data MsgMention = MsgMention {memberId :: MemberId} +newtype MsgMentions = MsgMentions (Map MemberName MsgMention) deriving (Eq, Show) $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MCL") ''MsgChatLink) +$(JQ.deriveJSON defaultJSON ''LinkOwnerSig) + $(JQ.deriveJSON defaultJSON ''MsgMention) +instance FromJSON MsgMentions where + parseJSON v = MsgMentions <$> parseJSON v + omittedField = Just $ MsgMentions M.empty + +instance ToJSON MsgMentions where + toJSON (MsgMentions m) = toJSON $ toMaybeMap m + toEncoding (MsgMentions m) = toEncoding $ toMaybeMap m + omitField (MsgMentions m) = M.null m + +toMaybeMap :: Map k v -> Maybe (Map k v) +toMaybeMap m = if M.null m then Nothing else Just m +{-# INLINE toMaybeMap #-} + $(JQ.deriveJSON defaultJSON ''QuotedMsg) +instance FromJSON MsgContent where + parseJSON (J.Object v) = + v .: "type" >>= \case + MCText_ -> MCText <$> v .: "text" + MCLink_ -> do + text <- v .: "text" + preview <- v .: "preview" + pure MCLink {text, preview} + MCImage_ -> do + text <- v .: "text" + image <- v .: "image" + pure MCImage {text, image} + MCVideo_ -> do + text <- v .: "text" + image <- v .: "image" + duration <- v .: "duration" + pure MCVideo {text, image, duration} + MCVoice_ -> do + text <- v .: "text" + duration <- v .: "duration" + pure MCVoice {text, duration} + MCFile_ -> MCFile <$> v .: "text" + MCReport_ -> do + text <- v .: "text" + reason <- v .: "reason" + pure MCReport {text, reason} + MCChat_ -> do + text <- v .: "text" + chatLink <- v .: "chatLink" + ownerSig <- v .:? "ownerSig" + pure MCChat {text, chatLink, ownerSig} + MCUnknown_ tag -> do + text <- fromMaybe unknownMsgType <$> v .:? "text" + pure MCUnknown {tag, text, json = v} + parseJSON invalid = + JT.prependFailure "bad MsgContent, " (JT.typeMismatch "Object" invalid) + +unknownMsgType :: Text +unknownMsgType = "unknown message type" + +(.=?) :: ToJSON v => JT.Key -> Maybe v -> [(J.Key, J.Value)] -> [(J.Key, J.Value)] +key .=? value = maybe id ((:) . (key .=)) value + +instance ToJSON MsgContent where + toJSON = \case + MCUnknown {json} -> J.Object json + MCText t -> J.object ["type" .= MCText_, "text" .= t] + MCLink {text, preview} -> J.object ["type" .= MCLink_, "text" .= text, "preview" .= preview] + MCImage {text, image} -> J.object ["type" .= MCImage_, "text" .= text, "image" .= image] + MCVideo {text, image, duration} -> J.object ["type" .= MCVideo_, "text" .= text, "image" .= image, "duration" .= duration] + MCVoice {text, duration} -> J.object ["type" .= MCVoice_, "text" .= text, "duration" .= duration] + MCFile t -> J.object ["type" .= MCFile_, "text" .= t] + MCReport {text, reason} -> J.object ["type" .= MCReport_, "text" .= text, "reason" .= reason] + MCChat {text, chatLink, ownerSig} -> J.object $ ("ownerSig" .=? ownerSig) ["type" .= MCChat_, "text" .= text, "chatLink" .= chatLink] + toEncoding = \case + MCUnknown {json} -> JE.value $ J.Object json + MCText t -> J.pairs $ "type" .= MCText_ <> "text" .= t + MCLink {text, preview} -> J.pairs $ "type" .= MCLink_ <> "text" .= text <> "preview" .= preview + MCImage {text, image} -> J.pairs $ "type" .= MCImage_ <> "text" .= text <> "image" .= image + MCVideo {text, image, duration} -> J.pairs $ "type" .= MCVideo_ <> "text" .= text <> "image" .= image <> "duration" .= duration + MCVoice {text, duration} -> J.pairs $ "type" .= MCVoice_ <> "text" .= text <> "duration" .= duration + MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t + MCReport {text, reason} -> J.pairs $ "type" .= MCReport_ <> "text" .= text <> "reason" .= reason + MCChat {text, chatLink, ownerSig} -> J.pairs $ "type" .= MCChat_ <> "text" .= text <> "chatLink" .= chatLink <> maybe mempty ("ownerSig" .=) ownerSig + +$(JQ.deriveJSON defaultJSON ''MsgContainer) + -- this limit reserves space for metadata in forwarded messages -- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, - 16 for block encryption ("rounded" to 15602) maxEncodedMsgLength :: Int @@ -835,109 +946,14 @@ markCompressedBatch :: ByteString -> ByteString markCompressedBatch = B.cons 'X' {-# INLINE markCompressedBatch #-} -parseMsgContainer :: J.Object -> JT.Parser MsgContainer -parseMsgContainer v = - MCQuote <$> v .: "quote" <*> mc - <|> MCComment <$> v .: "parent" <*> mc - <|> (v .: "forward" >>= \f -> (if f then MCForward else MCSimple) <$> mc) - -- The support for arbitrary object in "forward" property is added to allow - -- forward compatibility with forwards that include public group links. - <|> (MCForward <$> ((v .: "forward" :: JT.Parser J.Object) *> mc)) - <|> MCSimple <$> mc - where - mc = do - content <- v .: "content" - file <- v .:? "file" - ttl <- v .:? "ttl" - live <- v .:? "live" - mentions <- fromMaybe M.empty <$> (v .:? "mentions") - scope <- v .:? "scope" - asGroup <- v .:? "asGroup" - pure ExtMsgContent {content, mentions, file, ttl, live, scope, asGroup} - -extMsgContent :: MsgContent -> Maybe FileInvitation -> ExtMsgContent -extMsgContent mc file = ExtMsgContent mc M.empty file Nothing Nothing Nothing Nothing - justTrue :: Bool -> Maybe Bool justTrue True = Just True justTrue False = Nothing -instance FromJSON MsgContent where - parseJSON (J.Object v) = - v .: "type" >>= \case - MCText_ -> MCText <$> v .: "text" - MCLink_ -> do - text <- v .: "text" - preview <- v .: "preview" - pure MCLink {text, preview} - MCImage_ -> do - text <- v .: "text" - image <- v .: "image" - pure MCImage {text, image} - MCVideo_ -> do - text <- v .: "text" - image <- v .: "image" - duration <- v .: "duration" - pure MCVideo {text, image, duration} - MCVoice_ -> do - text <- v .: "text" - duration <- v .: "duration" - pure MCVoice {text, duration} - MCFile_ -> MCFile <$> v .: "text" - MCReport_ -> do - text <- v .: "text" - reason <- v .: "reason" - pure MCReport {text, reason} - MCChat_ -> do - text <- v .: "text" - chatLink <- v .: "chatLink" - pure MCChat {text, chatLink} - MCUnknown_ tag -> do - text <- fromMaybe unknownMsgType <$> v .:? "text" - pure MCUnknown {tag, text, json = v} - parseJSON invalid = - JT.prependFailure "bad MsgContent, " (JT.typeMismatch "Object" invalid) - -unknownMsgType :: Text -unknownMsgType = "unknown message type" - -msgContainerJSON :: MsgContainer -> J.Object -msgContainerJSON = \case - MCQuote qm mc -> o $ ("quote" .= qm) : msgContent mc - MCComment ref mc -> o $ ("parent" .= ref) : msgContent mc - MCForward mc -> o $ ("forward" .= True) : msgContent mc - MCSimple mc -> o $ msgContent mc - where - o = JM.fromList - msgContent ExtMsgContent {content, mentions, file, ttl, live, scope, asGroup} = - ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("mentions" .=? nonEmptyMap mentions) $ ("scope" .=? scope) $ ("asGroup" .=? asGroup) ["content" .= content] - nonEmptyMap :: Map k v -> Maybe (Map k v) nonEmptyMap m = if M.null m then Nothing else Just m {-# INLINE nonEmptyMap #-} -instance ToJSON MsgContent where - toJSON = \case - MCUnknown {json} -> J.Object json - MCText t -> J.object ["type" .= MCText_, "text" .= t] - MCLink {text, preview} -> J.object ["type" .= MCLink_, "text" .= text, "preview" .= preview] - MCImage {text, image} -> J.object ["type" .= MCImage_, "text" .= text, "image" .= image] - MCVideo {text, image, duration} -> J.object ["type" .= MCVideo_, "text" .= text, "image" .= image, "duration" .= duration] - MCVoice {text, duration} -> J.object ["type" .= MCVoice_, "text" .= text, "duration" .= duration] - MCFile t -> J.object ["type" .= MCFile_, "text" .= t] - MCReport {text, reason} -> J.object ["type" .= MCReport_, "text" .= text, "reason" .= reason] - MCChat {text, chatLink} -> J.object ["type" .= MCChat_, "text" .= text, "chatLink" .= chatLink] - toEncoding = \case - MCUnknown {json} -> JE.value $ J.Object json - MCText t -> J.pairs $ "type" .= MCText_ <> "text" .= t - MCLink {text, preview} -> J.pairs $ "type" .= MCLink_ <> "text" .= text <> "preview" .= preview - MCImage {text, image} -> J.pairs $ "type" .= MCImage_ <> "text" .= text <> "image" .= image - MCVideo {text, image, duration} -> J.pairs $ "type" .= MCVideo_ <> "text" .= text <> "image" .= image <> "duration" .= duration - MCVoice {text, duration} -> J.pairs $ "type" .= MCVoice_ <> "text" .= text <> "duration" .= duration - MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t - MCReport {text, reason} -> J.pairs $ "type" .= MCReport_ <> "text" .= text <> "reason" .= reason - MCChat {text, chatLink} -> J.pairs $ "type" .= MCChat_ <> "text" .= text <> "chatLink" .= chatLink - instance ToField MsgContent where toField = toField . encodeJSON @@ -1254,7 +1270,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do opt key = JT.parseEither (.:? key) params msg :: CMEventTag 'Json -> Either String (ChatMsgEvent 'Json) msg = \case - XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params + XMsgNew_ -> XMsgNew <$> JT.parseEither parseJSON (J.Object params) XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr" XMsgUpdate_ -> do msgId' <- p "msgId" @@ -1327,9 +1343,6 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XOk_ -> pure XOk XUnknown_ t -> pure $ XUnknown t params -(.=?) :: ToJSON v => JT.Key -> Maybe v -> [(J.Key, J.Value)] -> [(J.Key, J.Value)] -key .=? value = maybe id ((:) . (key .=)) value - chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @e of SBinary -> AMBinary AppMessageBinary {msgId = Nothing, tag = B.head $ strEncode tag, body = chatMsgBinaryToBody chatMsg} @@ -1340,7 +1353,9 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en o = JM.fromList params :: ChatMsgEvent 'Json -> J.Object params = \case - XMsgNew container -> msgContainerJSON container + XMsgNew mc -> case toJSON mc of + J.Object obj -> obj + _ -> JM.empty XMsgFileDescr msgId' fileDescr -> o ["msgId" .= msgId', "fileDescr" .= fileDescr] XMsgUpdate {msgId = msgId', content, mentions, ttl, live, scope, asGroup} -> o $ ("asGroup" .=? asGroup) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("scope" .=? scope) $ ("mentions" .=? nonEmptyMap mentions) ["msgId" .= msgId', "content" .= content] XMsgDel msgId' memberId scope -> o $ ("memberId" .=? memberId) $ ("scope" .=? scope) ["msgId" .= msgId'] diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 1aadfb507e..cfe8e944a5 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -79,7 +79,7 @@ minRemoteCtrlVersion = AppVersion [6, 5, 0, 12] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 4, 6, 0] +minRemoteHostVersion = AppVersion [6, 5, 0, 12] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 9128404244..60f898e52e 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -429,6 +429,14 @@ updatePreparedContactUser WHERE contact_profile_id = ? |] (newUserId, currentTs, profileId) + DB.execute + db + [sql| + UPDATE chat_items + SET user_id = ?, updated_at = ? + WHERE contact_id = ? + |] + (newUserId, currentTs, contactId) safeDeleteLDN db user oldLDN getContact db vr newUser contactId diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 93fdf1868a..c4e7210637 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -687,6 +687,14 @@ updatePreparedGroupUser db vr user gInfo@GroupInfo {groupId, membership} hostMem WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE group_id = ?) |] (newUserId, currentTs, groupId) + DB.execute + db + [sql| + UPDATE chat_items + SET user_id = ?, updated_at = ? + WHERE group_id = ? + |] + (newUserId, currentTs, groupId) safeDeleteLDN db user oldGroupLDN updateMembership GroupMember {groupMemberId = membershipId} currentTs = DB.execute diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index a2c91af86b..5d433088a4 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -229,7 +229,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent msgSigning_ encodeMessage ECMEncoded msgBody -> do let signedMsg_ = signBody <$> msgSigning_ signBody MsgSigning {bindingTag, bindingData, keyRef, privKey} = - let sig = C.ASignature C.SEd25519 $ C.sign' privKey (smpEncode bindingTag <> bindingData <> msgBody) + let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody) in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig :| [], signedBody = msgBody} createdAt <- getCurrentTime DB.execute @@ -240,7 +240,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent msgSigning_ encodeMessage shared_msg_id, shared_msg_id_user, created_at, updated_at ) VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - ((MDSnd, toCMEventTag chatMsgEvent, DB.Binary msgBody, chatBinding <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, connId_, groupId_) + ((MDSnd, toCMEventTag chatMsgEvent, DB.Binary msgBody, (\SignedMsg {chatBinding} -> chatBinding) <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, connId_, groupId_) :. (DB.Binary sharedMsgId, Just (BI True), createdAt, createdAt)) msgId <- insertedRowId db pure $ Right SndMessage {msgId, sharedMsgId = SharedMsgId sharedMsgId, msgBody, signedMsg_} @@ -327,7 +327,7 @@ createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, verifiedMsg, b shared_msg_id, author_group_member_id, forwarded_by_group_member_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, chatBinding <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, brokerTs, currentTs, currentTs, connId_, groupId_) + ((MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, (\SignedMsg {chatBinding} -> chatBinding) <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, brokerTs, currentTs, currentTs, connId_, groupId_) :. (sharedMsgId_, authorMember, forwardedByMember)) msgId <- insertedRowId db pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgSigned, forwardedByMember} @@ -558,7 +558,9 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, msgS quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg pure (ciId, quotedItem, itemForwarded) where - itemForwarded = cmForwardedFrom chatMsgEvent + itemForwarded = case chatMsgEvent of + ACME _ (XMsgNew MsgContainer {forward}) | forward == Just True -> Just CIFFUnknown + _ -> Nothing quotedMsg = cmToQuotedMsg chatMsgEvent quoteRow :: NewQuoteRow quoteRow = case quotedMsg of diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index a6f2d53aec..366074acc2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -446,6 +446,14 @@ Query: Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE chat_items + SET user_id = ?, updated_at = ? + WHERE group_id = ? + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_id (group_id=?) + Query: UPDATE contact_profiles SET display_name = ?, @@ -1063,6 +1071,14 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) +Query: + UPDATE chat_items + SET user_id = ?, updated_at = ? + WHERE contact_id = ? + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contact_id (contact_id=?) + Query: UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ? WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? @@ -1842,6 +1858,41 @@ SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_mem SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) +Query: + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, + user_id, local_display_name, contact_id, contact_profile_id, member_pub_key, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) +SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) +SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) + Query: INSERT INTO group_relays (group_id, group_member_id, chat_relay_id, relay_status, created_at, updated_at) @@ -6530,6 +6581,10 @@ Query: SELECT COUNT(1) FROM contacts WHERE user_id = ? AND chat_item_ttl > 0 Plan: SEARCH contacts USING INDEX idx_contacts_chat_ts (user_id=?) +Query: SELECT COUNT(1) FROM group_members WHERE member_role = 'owner' AND member_pub_key IS NOT NULL +Plan: +SCAN group_members + Query: SELECT COUNT(1) FROM groups WHERE user_id = ? AND chat_item_ttl > 0 Plan: SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) @@ -7092,6 +7147,10 @@ Query: UPDATE groups SET relay_own_status = ?, updated_at = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET relay_request_err_reason = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index ebe4820a11..6ddc411fff 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -52,7 +52,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink, ConnectionMode (..), ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) @@ -518,6 +518,18 @@ instance FromField BusinessChatType where fromField = fromTextField_ textDecode instance ToField BusinessChatType where toField = toField . textEncode +class HasShortLink l where + connShortLink' :: l c -> Maybe (ConnShortLink c) + +instance HasShortLink CreatedConnLink where + connShortLink' (CCLink _ sl) = sl + +setShortLinkType :: ContactConnType -> CreatedLinkContact -> CreatedLinkContact +setShortLinkType ct (CCLink cReq sl) = CCLink cReq (setShortLinkType_ ct <$> sl) + +setShortLinkType_ :: ContactConnType -> ShortLinkContact -> ShortLinkContact +setShortLinkType_ ct (CSLContact sch _ srv k) = CSLContact sch ct srv k + data PreparedGroup = PreparedGroup { connLinkToConnect :: CreatedLinkContact, connLinkPreparedConnection :: Bool, diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index 22cb73f325..e0630e2e42 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -83,6 +83,7 @@ data RelayStatus | RSInvited | RSAccepted | RSActive + | RSInactive deriving (Eq, Show) relayStatusText :: RelayStatus -> Text @@ -91,6 +92,7 @@ relayStatusText = \case RSInvited -> "invited" RSAccepted -> "accepted" RSActive -> "active" + RSInactive -> "inactive" instance TextEncoding RelayStatus where textEncode = \case @@ -98,11 +100,13 @@ instance TextEncoding RelayStatus where RSInvited -> "invited" RSAccepted -> "accepted" RSActive -> "active" + RSInactive -> "inactive" textDecode = \case "new" -> Just RSNew "invited" -> Just RSInvited "accepted" -> Just RSAccepted "active" -> Just RSActive + "inactive" -> Just RSInactive _ -> Nothing instance FromField RelayStatus where fromField = fromTextField_ textDecode diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 3afe0ce0ce..ce0f55cf01 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -22,7 +22,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) import Data.Function (on) import Data.Int (Int64) -import Data.List (groupBy, intercalate, intersperse, sortOn) +import Data.List (groupBy, intercalate, intersperse, nub, sortOn) import Data.List.NonEmpty (NonEmpty (..)) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -148,8 +148,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView - CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz - CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems + CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView + CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item <> viewTestInfo testView item) chatItems CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId] CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz @@ -180,6 +180,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRContactRequestRejected u UserContactRequest {localDisplayName = c} _ct_ -> ttyUser u [ttyContact c <> ": contact request rejected"] CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView + CRPublicGroupCreationFailed u results -> ttyUser u $ viewPublicGroupCreationFailed results CRGroupRelays u g relays -> ttyUser u $ viewGroupRelays g relays CRGroupMembers u g -> ttyUser u $ viewGroupMembers g CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms @@ -222,6 +223,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRGroupDeletedUser u g signed -> ttyUser u [ttyGroup' g <> ": you deleted the group" <> signedStr signed] CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc + CRChatMsgContent u mc -> ttyUser u $ ttyMsgContent mc <> viewMsgTestInfo testView mc CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts @@ -407,7 +409,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtContactRatchetSync u ct progress -> ttyUser u $ viewContactRatchetSync ct progress CEvtGroupMemberRatchetSync u g m progress -> ttyUser u $ viewGroupMemberRatchetSync g m progress CEvtChatInfoUpdated _ _ -> [] - CEvtNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz + CEvtNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView CEvtChatItemsStatusesUpdated u chatItems | length chatItems <= 20 -> concatMap @@ -646,11 +648,12 @@ viewChatItems :: [AChatItem] -> UTCTime -> TimeZone -> + Bool -> [StyledString] -viewChatItems ttyUser unmuted u chatItems ts tz +viewChatItems ttyUser unmuted u chatItems ts tz testView | length chatItems <= 20 = concatMap - (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item) + (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item <> viewTestInfo testView item) chatItems | all (\aci -> aChatItemDir aci == MDRcv) chatItems = ttyUser u [sShow (length chatItems) <> " new messages"] | all (\aci -> aChatItemDir aci == MDSnd) chatItems = ttyUser u [sShow (length chatItems) <> " messages sent"] @@ -949,6 +952,14 @@ viewItemReactions ChatItem {reactions} = [" " <> viewReactions reactions | viewReaction CIReactionCount {reaction = MREmoji (MREmojiChar emoji), userReacted, totalReacted} = plain [emoji, ' '] <> (if userReacted then styled Italic else plain) (show totalReacted) +viewTestInfo :: Bool -> ChatItem c d -> [StyledString] +viewTestInfo testView ChatItem {content} = maybe [] (viewMsgTestInfo testView) $ ciMsgContent content + +viewMsgTestInfo :: Bool -> MsgContent -> [StyledString] +viewMsgTestInfo testView = \case + MCChat {ownerSig = Just sig} | testView -> [viewJSON sig] + _ -> [] + viewReactionMembers :: [MemberReaction] -> [StyledString] viewReactionMembers memberReactions = [sShow (length memberReactions) <> " member(s) reacted"] @@ -1228,6 +1239,14 @@ viewGroupCreated g testView = where relaysInstruction = "wait for selected relay(s) to join, then you can invite members via group link" +viewPublicGroupCreationFailed :: [AddRelayResult] -> [StyledString] +viewPublicGroupCreationFailed results = + ["channel not created, results:"] + <> map showRelayResult results + where + showRelayResult (AddRelayResult UserChatRelay {chatRelayId = DBEntityId i} err_) = + " relay " <> sShow i <> ": " <> maybe "ok" (plain . tshow) err_ + viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString] viewCannotResendInvitation g c = [ ttyContact c <> " is already invited to group " <> ttyGroup' g, @@ -2039,7 +2058,7 @@ viewGroupUserChanged viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString] viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case CPInvitationLink ilp -> case ilp of - ILPOk contactSLinkData -> [invOrBiz contactSLinkData "ok to connect"] <> [viewJSON contactSLinkData | testView] + ILPOk contactSLinkData ov -> [invOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] ILPOwnLink -> [invLink "own link"] ILPConnecting Nothing -> [invLink "connecting"] ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)] @@ -2057,7 +2076,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("invitation link: " <>) CPContactAddress cap -> case cap of - CAPOk contactSLinkData -> [addrOrBiz contactSLinkData "ok to connect"] <> [viewJSON contactSLinkData | testView] + CAPOk contactSLinkData ov -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView] CAPOwnLink -> [ctAddr "own address"] CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] @@ -2075,9 +2094,10 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case | business -> ("business address: " <>) _ -> ("contact address: " <>) CPGroupLink glp -> case glp of - GLPOk groupSLinkInfo_ groupSLinkData -> + GLPOk groupSLinkInfo_ groupSLinkData ov -> let direct = maybe True (\(GroupShortLinkInfo {direct = d}) -> d) groupSLinkInfo_ in [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"] + <> viewSigVerification ov <> [viewJSON groupSLinkData | testView] GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] @@ -2099,6 +2119,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case "use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages" ] knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g + GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"] where connecting g = [grpOrBizLink g <> ": connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] grpLink = ("group link: " <>) @@ -2113,6 +2134,10 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case nextConnectPrepared Contact {preparedContact, activeConn} = case preparedContact of Just _ -> maybe True (\c -> connStatus c == ConnPrepared) activeConn _ -> False + viewSigVerification = \case + Just OVVerified -> ["owner signature: verified"] + Just (OVFailed r) -> ["owner signature: FAILED (" <> plain r <> ")"] + Nothing -> [] viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated @@ -2212,7 +2237,26 @@ sentWithTime_ ts tz styledMsg CIMeta {itemTs} = prependFirst (ttyMsgTime ts tz itemTs <> " ") styledMsg ttyMsgContent :: MsgContent -> [StyledString] -ttyMsgContent = msgPlain . msgContentText +ttyMsgContent = \case + MCChat {text, chatLink, ownerSig} -> + let (linkInfo, name, links) = viewChatLink chatLink + signed = if isJust ownerSig then " (signed)" else "" + body = if T.null text || text `elem` links then [] else msgPlain text + in [plain $ linkInfo <> viewName name <> signed <> ":"] <> map plain links <> body + mc -> msgPlain $ msgContentText mc + where + viewChatLink = \case + MCLGroup {connLink, groupProfile = GroupProfile {displayName, publicGroup}} -> + let (ref, links) = case publicGroup of + Just PublicGroupProfile {groupType, groupLink} -> (textEncode groupType, nub [enc connLink, enc groupLink]) + Nothing -> ("group", [enc connLink]) + in ("link to join " <> ref <> " #", displayName, links) + MCLContact {connLink, profile = Profile {displayName}} -> + ("contact address of @", displayName, [enc connLink]) + MCLInvitation {invLink, profile = Profile {displayName}} -> + ("one-time link of @", displayName, [enc invLink]) + enc :: StrEncoding a => a -> Text + enc = safeDecodeUtf8 . strEncode prependFirst :: StyledString -> [StyledString] -> [StyledString] prependFirst s [] = [s] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index ca2581eb01..714aa0c0ed 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -41,7 +41,8 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.FileTransfer.Description (kb, mb) import Simplex.FileTransfer.Server (runXFTPServerBlocking) -import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration) +import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), XFTPStoreConfig (..), defaultFileExpiration) +import Simplex.FileTransfer.Server.Store import Simplex.FileTransfer.Transport (alpnSupportedXFTPhandshakes, supportedFileServerVRange) import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite @@ -589,11 +590,12 @@ xftpTestPort = "7002" xftpServerFiles :: FilePath xftpServerFiles = "tests/tmp/xftp-server-files" -xftpServerConfig :: XFTPServerConfig +xftpServerConfig :: XFTPServerConfig STMFileStore xftpServerConfig = XFTPServerConfig { xftpPort = xftpTestPort, fileIdSize = 16, + serverStoreCfg = XSCMemory $ Just "tests/tmp/xftp-server-store.log", storeLogFile = Just "tests/tmp/xftp-server-store.log", filesPath = xftpServerFiles, fileSizeQuota = Nothing, @@ -628,7 +630,7 @@ xftpServerConfig = withXFTPServer :: IO () -> IO () withXFTPServer = withXFTPServer' xftpServerConfig -withXFTPServer' :: XFTPServerConfig -> IO () -> IO () +withXFTPServer' :: XFTPServerConfig STMFileStore -> IO () -> IO () withXFTPServer' cfg = serverBracket ( \started -> do diff --git a/tests/ChatTests/ChatRelays.hs b/tests/ChatTests/ChatRelays.hs index 721d71d0e0..58fe1074ef 100644 --- a/tests/ChatTests/ChatRelays.hs +++ b/tests/ChatTests/ChatRelays.hs @@ -1,9 +1,23 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} + module ChatTests.ChatRelays where import ChatClient import ChatTests.DBUtils +import ChatTests.Groups (memberJoinChannel, memberJoinChannel', prepareChannel, prepareChannel', prepareChannel1Relay, setupRelay) import ChatTests.Utils import Control.Concurrent (threadDelay) +import qualified Data.Aeson as J +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import ProtocolTests (testGroupProfile) +import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..)) +import Simplex.Chat.Types (GroupProfile (..)) +import Simplex.Messaging.Encoding.String (StrEncoding (..)) +import Simplex.Messaging.Util (decodeJSON) import Test.Hspec hiding (it) chatRelayTests :: SpecWith TestParams @@ -14,6 +28,10 @@ chatRelayTests = do it "re-add soft-deleted relay by same name" testReAddRelaySameName it "test chat relay" testChatRelayTest it "relay profile updated in address" testRelayProfileUpdateInAddress + describe "share channel card" $ do + it "share channel card in direct chat" testShareChannelDirect + it "share channel card in group" testShareChannelGroup + it "share channel card in channel" testShareChannelChannel testGetSetChatRelays :: HasCallStack => TestParams -> IO () testGetSetChatRelays ps = @@ -165,6 +183,151 @@ testRelayProfileUpdateInAddress ps = alice ##> ("/relay test " <> bobSLink) alice <## "relay test passed, profile: bob2 (Bob relay)" +testShareChannelDirect :: HasCallStack => TestParams -> IO () +testShareChannelDirect ps = + testChat3 aliceProfile bobProfile cathProfile test ps + where + test alice bob cath = withRelay ps $ \relay -> do + (shortLink, fullLink) <- prepareChannel1Relay "news" alice relay + connectUsers alice bob + -- alice gets ownerSig from share content API (for validation later) + alice ##> "/_share chat content #1 @2" + alice <## "link to join channel #news (signed):" + (_, apiOwnerSig) <- getTermLine2 alice + -- alice sends the card to bob + alice ##> "/share chat #news @bob" + alice <# "@bob link to join channel #news (signed):" + _ <- getTermLine2 alice -- alice's testView ownerSig + bob <# "alice> link to join channel #news (signed):" + -- bob captures the received ownerSig from message view (testView) + (sLink, cSig) <- getTermLine2 bob + sLink `shouldBe` shortLink + cSig `shouldBe` apiOwnerSig + -- bob verifies owner signature via connect plan + bob ##> ("/_connect plan 1 " <> shortLink <> " sig=" <> cSig) + bob <## "group link: ok to connect via relays" + bob <## "owner signature: verified" + _ <- getTermLine bob -- group link data + -- bob joins + memberJoinChannel' "news" 1 0 1 0 [relay] [alice] shortLink fullLink bob + connectUsers bob cath + -- bob (subscriber) shares unsigned - not owner + bob ##> "/share chat #news @cath" + bob <# "@cath link to join channel #news:" + _ <- getTermLine bob + cath <# "bob> link to join channel #news:" + _ <- getTermLine cath + -- bob tries to replay alice's signed card to cath - binding mismatch, sig stripped at receive + let sig = fromMaybe (error "bad sig") (decodeJSON (T.pack cSig) :: Maybe LinkOwnerSig) + cLink = either error id $ strDecode (B.pack sLink) + mc = MCChat (T.pack sLink) (MCLGroup cLink (testGroupProfile {displayName = "news"} :: GroupProfile)) (Just sig) + cm = "{\"msgContent\":" <> LB.unpack (J.encode mc) <> "}" + bob ##> ("/_send @3 json [" <> cm <> "]") + bob <# "@cath link to join group #news (signed):" + _ <- getTermLine2 bob -- bob's testView ownerSig (his sent has the sig data) + -- cath sees it without signature - binding was for alice->bob, not bob->cath, sig stripped + cath <# "bob> link to join group #news:" + _ <- getTermLine cath + -- cath joins anyway + memberJoinChannel "news" [relay] [alice] shortLink fullLink cath + alice #> "#news hello" + relay <# "#news> hello" + [bob, cath] *<# "#news> hello [>>]" + +testShareChannelGroup :: HasCallStack => TestParams -> IO () +testShareChannelGroup ps = + testChat3 aliceProfile bobProfile cathProfile test ps + where + test alice bob cath = withRelay ps $ \relay -> do + (shortLink, fullLink) <- prepareChannel1Relay "news" alice relay + createGroup2 "team" alice bob + alice ##> "/share chat #news #team" + alice <# "#team link to join channel #news:" + _ <- getTermLine alice + bob <# "#team alice> link to join channel #news:" + sLink <- getTermLine bob + sLink `shouldBe` shortLink + memberJoinChannel' "news" 2 0 1 0 [relay] [alice] sLink fullLink bob + createGroup2 "work" bob cath + bob ##> "/share chat #news #work" + bob <# "#work link to join channel #news:" + _ <- getTermLine bob + cath <# "#work bob> link to join channel #news:" + _ <- getTermLine cath + memberJoinChannel' "news" 2 0 0 0 [relay] [alice] shortLink fullLink cath + alice #> "#news hello" + relay <# "#news> hello" + [bob, cath] *<# "#news> hello [>>]" + +testShareChannelChannel :: HasCallStack => TestParams -> IO () +testShareChannelChannel ps = + testChat3 aliceProfile bobProfile cathProfile test ps + where + test alice bob cath = withRelay ps $ \relay -> do + relaySLink <- setupRelay alice relay + (sLink1, fLink1) <- prepareChannel "news" alice relay + (sLink2, fLink2) <- prepareChannel' 2 "updates" alice relay + -- bob joins "updates" first (relay doesn't know bob yet, no suffix) + memberJoinChannel "updates" [relay] [alice] sLink2 fLink2 bob + -- alice (owner) shares "news" to "updates" - signed + alice ##> "/_share chat content #1 #2(as_group=on)" + alice <## "link to join channel #news (signed):" + (apiLink, apiOwnerSig) <- getTermLine2 alice + apiLink `shouldBe` sLink1 + alice ##> "/share chat #news #updates" + alice <# "#updates link to join channel #news (signed):" + _ <- getTermLine2 alice -- link, ownerSig + relay <# "#updates> link to join channel #news (signed):" + _ <- getTermLine2 relay -- link, ownerSig + bob <# "#updates> link to join channel #news (signed): [>>]" + (cLink, cSig) <- getTermLine2 bob + cLink `shouldBe` (sLink1 <> " [>>]") + cSig `shouldBe` apiOwnerSig + -- bob verifies alice's signature via connect plan + bob ##> ("/_connect plan 1 " <> sLink1 <> " sig=" <> cSig) + bob <## "group link: ok to connect via relays" + bob <## "owner signature: verified" + _ <- getTermLine bob -- group link data + -- bob joins "news" (group #2 for bob, relay knows bob from "updates" so sfx=1) + memberJoinChannel' "news" 2 1 1 1 [relay] [alice] sLink1 fLink1 bob + -- bob creates channel "bob_ch" for delivery to cath + bob ##> ("/relays name=relay " <> relaySLink) + bob <## "ok" + (sLink3, fLink3) <- prepareChannel "bob_ch" bob relay + memberJoinChannel "bob_ch" [relay] [bob] sLink3 fLink3 cath + -- bob (subscriber) shares "news" to "bob_ch" - unsigned (not owner) + bob ##> "/share chat #news #bob_ch" + bob <# "#bob_ch link to join channel #news:" + _ <- getTermLine bob + relay <# "#bob_ch> link to join channel #news:" + _ <- getTermLine relay + cath <# "#bob_ch> link to join channel #news: [>>]" + _ <- getTermLine cath + -- bob tries to replay alice's signed card to bob_ch - binding mismatch, sig stripped at receive + let sig = fromMaybe (error "bad sig") (decodeJSON (T.pack cSig) :: Maybe LinkOwnerSig) + cLink' = either error id $ strDecode (B.pack sLink1) + mc = MCChat (T.pack sLink1) (MCLGroup cLink' (testGroupProfile {displayName = "news"} :: GroupProfile)) (Just sig) + cm = "{\"msgContent\":" <> LB.unpack (J.encode mc) <> "}" + bob ##> ("/_send #3 json [" <> cm <> "]") + bob <# "#bob_ch link to join group #news (signed):" + _ <- getTermLine2 bob -- bob's testView ownerSig (his sent has the sig data) + relay <# "#bob_ch bob_2> link to join group #news:" + _ <- getTermLine relay + cath <# "#bob_ch bob> link to join group #news: [>>]" + _ <- getTermLine cath + -- cath joins "news" (group #2 for cath since "bob_ch" is #1) + memberJoinChannel' "news" 2 1 0 1 [relay] [alice] sLink1 fLink1 cath + -- alice sends message, both receive + alice #> "#news hello" + relay <# "#news> hello" + [bob, cath] *<# "#news> hello [>>]" + +getTermLine2 :: TestCC -> IO (String, String) +getTermLine2 c = (,) <$> getTermLine c <*> getTermLine c + +withRelay :: HasCallStack => TestParams -> (TestCC -> IO ()) -> IO () +withRelay ps = withNewTestChatOpts ps relayTestOpts "relay" relayProfile + -- Create a public group with relay=1, wait for relay to join createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO () createChannelWithRelay gName owner relay = do diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index aedbb5e1e9..1a1bc4b82f 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -253,6 +253,7 @@ chatGroupTests = do it "should update channel profile (signed)" testChannelUpdateProfileSigned it "should preserve working link after profile update" testChannelLinkAfterProfileUpdate it "should preserve working link after welcome message update" testChannelLinkAfterWelcomeUpdate + it "should preserve owner key in link data after profile update" testChannelOwnerKeyAfterLinkUpdate it "should update channel preferences (signed)" testChannelUpdatePrefsSigned it "should change member role (signed)" testChannelChangeRoleSigned it "should block member for all (signed)" testChannelBlockMemberSigned @@ -261,8 +262,10 @@ chatGroupTests = do it "should delete channel and clean up relay connections" testChannelDeleteGroupCleanup it "owner should leave channel (signed)" testChannelOwnerLeave it "subscriber should leave channel (signed)" testChannelSubscriberLeave + it "relay should leave channel" testChannelRelayLeave it "owner should update profile in channel (signed)" testChannelOwnerProfileUpdate it "subscriber should update profile in channel (signed)" testChannelSubscriberProfileUpdate + it "should report relay results when one relay deleted its address" testChannelCreateDeletedRelay describe "channel message operations" $ do it "should update channel message" testChannelMessageUpdate it "should delete channel message" testChannelMessageDelete @@ -8439,16 +8442,25 @@ createChannel1Relay gName owner relay cath dan eve = do forM_ [cath, dan, eve] $ \member -> memberJoinChannel gName [relay] [owner] shortLink fullLink member -prepareChannel1Relay :: String -> TestCC -> TestCC -> IO (String, String) -prepareChannel1Relay gName owner relay = do +setupRelay :: TestCC -> TestCC -> IO String +setupRelay owner relay = do rName <- userName relay - relay ##> "/ad" (relaySLink, _cLink) <- getContactLinks relay True - owner ##> ("/relays name=" <> rName <> " " <> relaySLink) owner <## "ok" + pure relaySLink +prepareChannel1Relay :: String -> TestCC -> TestCC -> IO (String, String) +prepareChannel1Relay gName owner relay = do + _ <- setupRelay owner relay + prepareChannel gName owner relay + +prepareChannel :: String -> TestCC -> TestCC -> IO (String, String) +prepareChannel = prepareChannel' 1 + +prepareChannel' :: Int -> String -> TestCC -> TestCC -> IO (String, String) +prepareChannel' relayId gName owner relay = do owner ##> ("/public group relays=1 #" <> gName) owner <## ("group #" <> gName <> " is created") owner <## "wait for selected relay(s) to join, then you can invite members via group link" @@ -8456,7 +8468,7 @@ prepareChannel1Relay gName owner relay = do concurrentlyN_ [ do owner <## ("#" <> gName <> ": group link relays updated, current relays:") - owner <## " - relay id 1: active" + owner <## (" - relay id " <> show relayId <> ": active") owner <## "group link:" _ <- getTermLine owner pure (), @@ -8515,10 +8527,17 @@ prepareChannel2Relays gName owner relay1 relay2 = do getGroupLinks owner gName GRMember False memberJoinChannel :: String -> [TestCC] -> [TestCC] -> String -> String -> TestCC -> IO () -memberJoinChannel gName relays owners shortLink fullLink member = do +memberJoinChannel gName = memberJoinChannel' gName 1 0 0 0 + +-- | sfx params: relaySfx - how relay/owner see the member, memberRelaySfx - how member sees relay +memberJoinChannel' :: String -> Int -> Int -> Int -> Int -> [TestCC] -> [TestCC] -> String -> String -> TestCC -> IO () +memberJoinChannel' gName gId relaySfx ownerSfx memberRelaySfx relays owners shortLink fullLink member = do mName <- userName member mFullName <- showName member - relayNames <- mapM userName relays + let sfxMName s = if s == 0 then mName else mName <> "_" <> show s + sfxName s = if s == 0 then mFullName else sfxMName s <> drop (length mName) mFullName + sfxRelayName rn = if memberRelaySfx == 0 then rn else rn <> "_" <> show memberRelaySfx + relayNames <- mapM (\r -> sfxRelayName <$> userName r) relays member ##> ("/_connect plan 1 " <> shortLink) member <## "group link: ok to connect via relays" @@ -8527,7 +8546,7 @@ memberJoinChannel gName relays owners shortLink fullLink member = do member ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " direct=off " <> groupSLinkData) member <## ("#" <> gName <> ": group is prepared") - member ##> "/_connect group #1" + member ##> ("/_connect group #" <> show gId) member <## ("#" <> gName <> ": connection started") concurrentlyN_ $ [ member @@ -8539,11 +8558,11 @@ memberJoinChannel gName relays owners shortLink fullLink member = do ] ] <> [ do - relay <## (mFullName <> ": accepting request to join group #" <> gName <> "...") - relay <## ("#" <> gName <> ": " <> mName <> " joined the group") + relay <## (sfxName relaySfx <> ": accepting request to join group #" <> gName <> "...") + relay <## ("#" <> gName <> ": " <> sfxMName relaySfx <> " joined the group") | relay <- relays ] - <> [ owner <### [EndsWith ("added " <> mFullName <> " to the group")] + <> [ owner <### [EndsWith ("added " <> sfxName ownerSfx <> " to the group")] | owner <- owners ] @@ -8844,6 +8863,60 @@ testChannelLinkAfterWelcomeUpdate ps = bob <# "#team> hi" [cath, dan] *<# "#team> hi [>>]" +testChannelOwnerKeyAfterLinkUpdate :: HasCallStack => TestParams -> IO () +testChannelOwnerKeyAfterLinkUpdate ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + threadDelay 100000 + + -- Owner updates channel profile - triggers rebuilding link data. + alice ##> "/gp team my_team My team description" + alice <## "changed to #my_team (My team description)" + concurrentlyN_ + [ do + bob <## "alice updated group #team: (signed)" + bob <## "changed to #my_team (My team description)", + do + cath <## "alice updated group #team: (signed)" + cath <## "changed to #my_team (My team description)" + ] + + threadDelay 100000 + + -- Late subscriber joins via the same channel link after profile update. + alice ##> "/show link #my_team" + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + shortLink' `shouldBe` shortLink + fullLink' `shouldBe` fullLink + memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan + + threadDelay 100000 + + -- Verify owner member record in late subscriber's DB has a public key. + ownerKeyPresent <- withCCTransaction dan $ \db -> + DB.query_ db "SELECT COUNT(1) FROM group_members WHERE member_role = 'owner' AND member_pub_key IS NOT NULL" :: IO [[Int]] + ownerKeyPresent `shouldBe` [[1]] + + -- Verify signed event is received by late subscriber. + alice ##> "/gp my_team team" + alice <## "changed to #team" + concurrentlyN_ + [ do + bob <## "alice updated group #my_team: (signed)" + bob <## "changed to #team", + do + cath <## "alice updated group #my_team: (signed)" + cath <## "changed to #team", + do + dan <## "alice updated group #my_team: (signed)" + dan <## "changed to #team" + ] + testChannelUpdatePrefsSigned :: HasCallStack => TestParams -> IO () testChannelUpdatePrefsSigned ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9259,6 +9332,78 @@ testChannelSubscriberLeave ps = DB.query db "SELECT member_status FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] map (\(Only s) -> s) statuses `shouldBe` maybeToList expected +testChannelRelayLeave :: HasCallStack => TestParams -> IO () +testChannelRelayLeave ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + forM_ [dan, eve] $ \member -> + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink member + + -- verify channel works + alice #> "#team hello" + [bob, cath] *<# "#team> hello" + [dan, eve] *<# "#team> hello [>>]" + + -- relay1 (bob) leaves + threadDelay 100000 + bob ##> "/leave #team" + bob <## "#team: you left the group" + bob <## "use /d #team to delete the group" + concurrentlyN_ + [ alice <## "#team: bob left the group (signed)", + -- cath: not notified (relays not connected, owner doesn't forward) + dan <## "#team: bob left the group (signed)", + eve <## "#team: bob left the group (signed)" + ] + + -- verify relay1 member status is "left" on all clients that know bob + checkMemberStatus alice "bob" (Just "left") + checkMemberStatus dan "bob" (Just "left") + checkMemberStatus eve "bob" (Just "left") + + -- verify channel still works with remaining relay + threadDelay 100000 + alice #> "#team still working" + cath <# "#team> still working" + [dan, eve] *<# "#team> still working [>>]" + + -- relay2 (cath) leaves + threadDelay 100000 + cath ##> "/leave #team" + cath <## "#team: you left the group" + cath <## "use /d #team to delete the group" + concurrentlyN_ + [ alice <## "#team: cath left the group (signed)", + dan <## "#team: cath left the group (signed)", + eve <## "#team: cath left the group (signed)" + ] + + -- verify relay2 member status + checkMemberStatus alice "cath" (Just "left") + checkMemberStatus dan "cath" (Just "left") + checkMemberStatus eve "cath" (Just "left") + + -- verify no delivery: owner sends but no relays to forward + alice #> "#team no delivery" + (dan ("/_connect plan 1 " <> shortLink) + frank <## "group link: channel has no active relays, please try to join later" + where + checkMemberStatus :: HasCallStack => TestCC -> T.Text -> Maybe T.Text -> IO () + checkMemberStatus cc name expected = do + statuses <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_status FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] + map (\(Only s) -> s) statuses `shouldBe` maybeToList expected + testChannelOwnerProfileUpdate :: HasCallStack => TestParams -> IO () testChannelOwnerProfileUpdate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9395,6 +9540,33 @@ testChannelSubscriberProfileUpdate ps = dan `hasContactProfiles` ["alice", "bob", "kate", "dave"] eve `hasContactProfiles` ["alice", "bob", "kate", "dave", "eve"] +testChannelCreateDeletedRelay :: HasCallStack => TestParams -> IO () +testChannelCreateDeletedRelay ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> do + bob ##> "/ad" + (bobSLink, _) <- getContactLinks bob True + cath ##> "/ad" + (cathSLink, _) <- getContactLinks cath True + + alice ##> ("/relays name=bob " <> bobSLink <> " name=cath " <> cathSLink) + alice <## "ok" + + -- cath deletes her address - simulates relay becoming unavailable + cath ##> "/da" + cath <## "Your chat address is deleted - accepted contacts will remain connected." + cath <## "To create a new chat address use /ad" + + -- channel creation fails because one relay's address was deleted + alice ##> "/public group relays=1,2 #team" + alice <## "channel not created, results:" + alice <## " relay 1: ok" + alice <##. " relay 2: ChatErrorAgent" + -- deleteInProgressGroup deletes relay connection alice joined on bob; + -- bob's agent reports AUTH error when the queue is gone — drain it. + void $ getTermLine bob + testChannelMessageUpdate :: HasCallStack => TestParams -> IO () testChannelMessageUpdate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 8acdb78b34..ebc9056164 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -81,6 +81,9 @@ frankProfile = mkProfile "frank" "Frank" Nothing businessProfile :: Profile businessProfile = mkProfile "biz" "Biz Inc" Nothing +relayProfile :: Profile +relayProfile = mkProfile "relay" "Relay" Nothing + mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs} diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 6d9ee54e6c..3ba789b988 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -113,84 +113,79 @@ decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do it "x.msg.new simple text" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" - #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)) + #==# XMsgNew (mcSimple (MCText "hello")) it "x.msg.new simple text - timed message TTL" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing Nothing Nothing)) + #==# XMsgNew ((mcSimple (MCText "hello")) {ttl = Just 3600}) it "x.msg.new simple text - live message" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True) Nothing Nothing)) + #==# XMsgNew ((mcSimple (MCText "hello")) {live = Just True}) it "x.msg.new simple link" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}" - #==# XMsgNew (MCSimple (extMsgContent (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing}) Nothing)) + #==# XMsgNew (mcSimple (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing})) it "x.msg.new simple image" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" - #==# XMsgNew (MCSimple (extMsgContent (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing)) + #==# XMsgNew (mcSimple (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=")) it "x.msg.new simple image with text" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" - #==# XMsgNew (MCSimple (extMsgContent (MCImage "here's an image" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing)) + #==# XMsgNew (mcSimple (MCImage "here's an image" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=")) it "x.msg.new chat message" $ "{\"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))) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new chat message with chat version range" $ "{\"v\":\"1-17\",\"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))) + ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) 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 chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") - (XMsgNew (MCQuote quotedMsg (extMsgContent (MCText "hello to you too") Nothing))) + (XMsgNew (mcQuote quotedMsg (MCText "hello to you too"))) it "x.msg.new quote - timed message TTL" $ "{\"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\"}},\"ttl\":3600}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") - (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing (Just 3600) Nothing Nothing Nothing))) + (XMsgNew ((mcQuote quotedMsg (MCText "hello to you too")) {ttl = Just 3600})) it "x.msg.new quote - live message" $ "{\"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\"}},\"live\":true}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") - (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing Nothing (Just True) Nothing Nothing))) + (XMsgNew ((mcQuote quotedMsg (MCText "hello to you too")) {live = Just True})) it "x.msg.new forward" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}" - ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ mcForward (MCText "hello")) it "x.msg.new forward - timed message TTL" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}" - ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing Nothing Nothing)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {ttl = Just 3600}) it "x.msg.new forward - live message" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}" - ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True) Nothing Nothing)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {live = Just True}) it "x.msg.new simple text with file" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" - #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) + #==# XMsgNew ((mcSimple (MCText "hello")) {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.new simple file with file" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}" - #==# XMsgNew (MCSimple (extMsgContent (MCFile "") (Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) + #==# XMsgNew ((mcSimple (MCFile "")) {file = Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.new quote with file" $ - "{\"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\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") ( XMsgNew - ( MCQuote - quotedMsg - ( extMsgContent - (MCText "hello to you too") - (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}) - ) - ) + (mcQuote quotedMsg (MCText "hello to you too")) + {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}} ) it "x.msg.new report" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"reason\":\"spam\",\"type\":\"report\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") - (XMsgNew (MCQuote quotedMsg (extMsgContent (MCReport "" RRSpam) Nothing))) + (XMsgNew (mcQuote quotedMsg (MCReport "" RRSpam))) it "x.msg.new forward with file" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" - ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"forward\":true}}" + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.update" $ "{\"v\":\"1\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") [] Nothing Nothing Nothing Nothing @@ -300,7 +295,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do -- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" -- #==# XGrpMsgForward -- (MemberId "\1\2\3\4") - -- (ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))) + -- (ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello")))) -- (systemToUTCTime $ MkSystemTime 1 1) it "x.info.probe" $ "{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}" diff --git a/website/langs/ar.json b/website/langs/ar.json index 41825d0683..0124dfebb4 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -284,13 +284,14 @@ "index-token-p1": "ستدعم مجموعاتك المفضلة بقسائم المجتمع المستقبلية.", "index-token-p2": "ستدفع القسائم ثمن الخوادم، لتمكين مجتمعاتك من البقاء حرة ومستقلة.", "index-roadmap-h2": "خارطة طريق SimpleX للإنترنت المجاني", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "التوسع إلى مجتمعات كبيرة", - "index-roadmap-2025-desc": "الهروب من المنصات المركزية", - "index-roadmap-2026-title": "مجتمعات وخوادم مستدامة", - "index-roadmap-2026-desc": "إطلاق قسائم المجتمع", - "index-roadmap-2027-title": "اجعل مجتمعاتك تنمو", - "index-roadmap-2027-desc": "أدوات لتعزيز مجتمعاتك", + "index-roadmap-now": "الآن", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "التوسع إلى مجتمعات كبيرة", + "index-roadmap-1-desc": "الهروب من المنصات المركزية", + "index-roadmap-2-title": "مجتمعات وخوادم مستدامة", + "index-roadmap-2-desc": "إطلاق قسائم المجتمع", + "index-roadmap-3-title": "اجعل مجتمعاتك تنمو", + "index-roadmap-3-desc": "أدوات لتعزيز مجتمعاتك", "index-directory-h2": "انضم إلى مجتمعات SimpleX", "index-directory-p1": "مئات الآلاف من الأشخاص يثقون بالفعل في مُراسلة SimpleX.", "index-directory-p2": "ابحث عن مجتمعاتك في دليل SimpleX وأنشئ مجتمعك الخاص!", @@ -309,8 +310,8 @@ "messengers-comparison-section-list-point-4": "تطبيقات الأجهزة المتعددة تعرض أمن Double Ratchet بعد الاختراق للخطر", "messengers-comparison-section-list-point-5": "تبادل المفاتيح الثنائي اختياري عبر التحقق من رمز الأمان.", "messengers-comparison-section-list-point-6": "اتفاقية مفتاح ما بعد الكم \"متفرقة\" — فهي تحمي بعض خطوات ratchet فقط.", - "index-roadmap-2026": "2026", - "index-roadmap-2027": "2027", + "index-roadmap-2": "يونيو 2027", + "index-roadmap-3": "ديسمبر 2027", "navbar-token": "رمز", "index-token-cta": "تعرف على المزيد واحصل على NFT مجاني
للاختبار المبكر.", "navbar-old-site": "الموقع القديم", diff --git a/website/langs/cs.json b/website/langs/cs.json index 19f4aeedb6..41b026fb58 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -6,7 +6,7 @@ "hero-overlay-card-1-p-3": "Definujete, které servery se mají používat k přijímání zpráv, vašich kontaktů — servery, které používáte k odesílání zpráv. Každá konverzace bude pravděpodobně používat dva různé servery.", "hero-overlay-card-2-p-3": "I v těch nejsoukromějších aplikacích, které používají služby Tor v3, pokud mluvíte se dvěma různými kontakty prostřednictvím stejného profilu, může být prokázáno, že jsou spojeni se stejnou osobou.", "simplex-network-overlay-card-1-p-1": "P2P protokoly a aplikace pro zasílání zpráv mají různé problémy, které je činí méně spolehlivými než SimpleX, složitějšími na analýzu a zranitelnými vůči několika typům útoků.", - "simplex-network-overlay-card-1-li-1": "P2P sítě spoléhají na nějakou variantu DHT pro směrování zpráv. Návrhy DHT musí vyvážit záruku dodávky a latenci. SimpleX má lepší záruku doručení a nižší latenci než P2P, protože zpráva může být zároveň předávána přes několik serverů pomocí serverů vybraných příjemcem. V P2P sítích je zpráva předávána přes uzly O(log N) postupně pomocí uzlů vybraných algoritmem.", + "simplex-network-overlay-card-1-li-1": "P2P sítě spoléhají na varianty DHT pro směrování zpráv. Tyto implementace DHT musí vyvážit záruku dodávky a latenci. SimpleX má lepší záruku doručení a nižší latenci než P2P, protože zpráva může být zároveň předávána přes několik serverů, které si příjemce vybere. V P2P sítích je zpráva předávána přes uzly O(log N) postupně/sekvenčně pomocí uzlů vybraných algoritmem.", "home": "Úvod", "developers": "Vývojáři", "reference": "Odkazy", @@ -193,7 +193,7 @@ "simplex-explained-tab-1-p-1": "Můžete vytvářet kontakty a skupiny a vést obousměrné konverzace, stejně jako v jakémkoli jiném messengeru.", "simplex-explained-tab-3-p-2": "Uživatelé mohou dále zlepšit soukromí metadat pomocí Tor pro přístup k serverům, což zabraňuje korelaci podle IP adresy.", "hero-p-1": "Jiné aplikace mají uživatelská ID: Signal, Matrix, Session, Briar, Jami, Cwtch atd.
SimpleX ne, ani náhodná čísla.
To radikálně zlepšuje vaše soukromí.", - "hero-2-header-desc": "Video ukazuje, jak se spojit se svým přítelem prostřednictvím jeho jednorázového QR kódu, osobně nebo prostřednictvím QR kódu ve videu. Můžete se také připojit sdílením pozvánky.", + "hero-2-header-desc": "Video ukazuje, jak se spojit se svým přítelem prostřednictvím jeho jednorázového QR kódu, osobně nebo prostřednictvím QR kódu ve videu. Můžete se také připojit sdílením odkazu pozvánky.", "feature-2-title": "E2E šifrované
obrázky, videa a soubory", "feature-5-title": "Mizící tajné konverzace", "simplex-private-1-title": "2-vrstvé
end-to-end šifrování", @@ -208,11 +208,11 @@ "if-you-already-installed-simplex-chat-for-the-terminal": "Pokud jste již nainstalovali SimpleX Chat pro terminál", "if-you-already-installed": "Pokud jste již nainstalovali", "simplex-network-1-desc": "Všechny zprávy se odesílají přes servery, což zajišťuje lepší soukromí metadat a spolehlivé asynchronní doručování zpráv, přičemž se zamezuje mnoha", - "simplex-private-card-1-point-1": "Double-ratchet protokol —
OTR messaging s dokonalým dopředným utajením a obnovou po vloupání.", + "simplex-private-card-1-point-1": "Double-ratchet protokol —
OTR zprávy s perfect forward secrecy a obnovou po narušení.", "guide-dropdown-1": "Rychlý start", "guide-dropdown-2": "Odesílání zpráv", "guide-dropdown-3": "Tajné skupiny", - "guide-dropdown-4": "Chat profily", + "guide-dropdown-4": "Profily chatu", "guide-dropdown-5": "Správa dat", "guide-dropdown-6": "Audio a video hovory", "guide-dropdown-7": "Soukromí a bezpečnost", @@ -236,7 +236,7 @@ "hero-overlay-3-textlink": "Hodnocení zabezpečení", "hero-overlay-card-3-p-1": "Trail of Bits je přední bezpečnostní a technologické poradenství, jejichž klienti zahrnují velké technologické firmy, vládní agentury a významné blockchainové projekty.", "f-droid-page-simplex-chat-repo-section-text": "Chcete-li jej přidat do vašeho F-Droid clienta, naskenujte QR kód nebo použijte tuto adresu URL:", - "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat a F-Droid.org repozitáře jsou podepsané různými klíči. Chcete-li přepnout, prosím exportujte chat databázi a přeinstalujte aplikaci.", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat a F-Droid.org repozitáře jsou podepsané různými klíči. Chcete-li mezi nimi přejít, prosím exportujte chat databázi a přeinstalujte aplikaci.", "comparison-section-list-point-4a": "SimpleX relé nemůže ohrozit šifrování e2e. Ověřte bezpečnostní kód, který zmírňuje mimo pásmový útok na kanál", "docs-dropdown-8": "SimpleX Directory", "please-enable-javascript": "Prosím, povolte JavaScript k zobrazení QR kódu.", @@ -286,15 +286,16 @@ "index-token-p2": "Vouchery budou sloužit k úhradě provozu serverů, aby skupiny zůstaly svobodné a nezávislé.", "index-token-cta": "Zjistěte více a získejte bezplatnou vstupenku pro rané testování.", "index-roadmap-h2": "Plán SimpleX ke svobodnému internetu", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Škálování pro velké komunity", - "index-roadmap-2025-desc": "Odchod od centralizovaných platforem", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Udržitelné komunity a servery", - "index-roadmap-2026-desc": "Spuštění Skupinových Voucherů", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Pomozte své komunitě růst", - "index-roadmap-2027-desc": "Nástroje na podporu vašich komunit", + "index-roadmap-now": "Nyní", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Škálování pro velké komunity", + "index-roadmap-1-desc": "Odchod od centralizovaných platforem", + "index-roadmap-2": "Červen 2027", + "index-roadmap-2-title": "Udržitelné komunity a servery", + "index-roadmap-2-desc": "Spuštění Skupinových Voucherů", + "index-roadmap-3": "Prosinec 2027", + "index-roadmap-3-title": "Pomozte své komunitě růst", + "index-roadmap-3-desc": "Nástroje na podporu vašich komunit", "index-directory-h2": "Zapojte se do komunit SimpleX", "index-directory-p1": "Statisíce lidí už důvěřují komunikaci přes SimpleX.", "index-directory-p2": "Najděte své komunity v katalogu SimpleX a vytvořte si vlastní!", @@ -315,13 +316,13 @@ "messengers-comparison-section-list-point-6": "Postkvantová dohoda o klíči je omezená — chrání pouze některé kroky ratchet mechanismu.", "navbar-old-site": "Starý web", "why-p1": "Narodili jste se bez účtu.", - "why-p2": "Nikdo nesledoval vaše konverzace. Nikdo nenakreslil mapu, kde jste byli. Ochrana osobních údajů nikdy nebyla funkce — byl to způsob života.", - "why-tagline": "Buďte volní ve své síti.", - "why-footer-link": "Proč ji stavíme", + "why-p2": "Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí — byl to způsob života.", + "why-tagline": "Buďte svobodní ve své síti.", + "why-footer-link": "Proč ji vyvíjíme", "docs-dropdown-15": "Ověřitelné a opakovatelné sestavení", "why-p3": "Pak jsme se přesunuli na internet a každá platforma chtěla o vás něco vědět — vaše jméno, vaše číslo, vaše přátele. Smířili jsme se s tím, že cenou za komunikaci s ostatními je dát někomu vědět, s kým mluvíme. Každá generace, lidská i technická, to tak měla — telefon, e-mail, komunikátory, sociální sítě. Zdálo se, že je to jediný možný způsob.", "why-p4": "Existuje i jiný způsob. Síť bez telefonních čísel. Bez uživatelských jmen. Bez účtů. Bez jakékoli uživatelské identity. Síť, která spojuje lidi a přenáší šifrované zprávy, aniž by bylo známo, kdo je připojen.", - "why-p5": "Není lepší zámek na dveřích někoho jiného. Není milejší nájemce, který respektuje vaše soukromí, ale přesto vede evidenci všech návštěvníků. Vy nejste host. Jste doma. Ani král do něj nemůže vstoupit — jste suverén.", + "why-p5": "Nejde o to mít lepší zámek na dveřích někoho jiného. Ani o to mít nájemce, který respektuje vaše soukromí, ale vede evidenci všech vašich návštěvníků. Nejste host. Jste doma. Ani král k vám nemůže vstoupit — jste suverén.", "why-p6": "Vaše konverzace patří vám, jako tomu bylo vždy před internetem. Síť není místo, které navštěvujete. Je to místo, které vytváříte a vlastníte. A nikdo vám ho nemůže vzít, ať už je soukromé, nebo veřejné.", "why-p7": "Nejstarší lidská svoboda — mluvit s druhým člověkem, aniž by byl sledován — postavena na infrastruktuře, která ji nemůže zradit.", "why-p8": "Protože jsme zničili sílu vědět, kdo jste. Aby vám vaši moc nikdo nemohl vzít.", @@ -336,7 +337,7 @@ "file-drop-text": "Přetáhněte soubor sem", "file-drop-hint": "nebo", "file-choose": "Vyberte soubor", - "file-max-size": "Max 100 MB - SimpleX Chat apka podporuje soubory až do 1 GB", + "file-max-size": "Max. 100 MB - SimpleX Chat aplikace podporuje soubory do 1 GB", "file-encrypting": "Šifruji…", "file-uploading": "Nahrávám…", "file-cancel": "Zrušit", @@ -347,7 +348,7 @@ "file-expiry": "Soubory jsou obvykle k dispozici 48 hodin.", "file-sec-1": "Váš soubor byl zašifrován v prohlížeči - datové routery nikdy neuvidí obsah souboru, jméno nebo velikost.", "file-sec-2": "Šifrovací klíč je obsažen v hash části odkazu – nikdy se neodesílá na žádný server.", - "file-sec-3": "Pro větší bezpečnost, použijte SimpleX Chat apku.", + "file-sec-3": "Pro vyšší bezpečnost použijte aplikaci SimpleX Chat.", "file-retry": "Znovu", "file-downloading": "Stahuji…", "file-decrypting": "Dešifruji…", @@ -359,14 +360,14 @@ "file-init-error": "Chyba inicializace: %error%", "file-available": "Dostupný soubor (~%size%)", "file-dl-sec-1": "Tento soubor je šifrován - datové směrovače nikdy neuvidí obsah souboru, jméno nebo velikost.", - "file-workers-required": "Vyžadováni Web Workers — aktualizujte prohlížeč", + "file-workers-required": "Vyžadovány Web Workers — aktualizujte svůj prohlížeč", "file-protocol-title": "XFTP protokol: nejbezpečnější přenos souborů", "file-proto-h-1": "Není nutný žádný účet", "file-proto-p-1": "Každá část souborů používá nový náhodný klíč. Datové směrovače nemají \"uživatele\" nebo \"soubory\" - přenášejí šifrované části souborů stejných velikostí.", "file-proto-h-2": "Trojitě zašifrováno ve vašem prohlížeči", "file-proto-h-4": "Nezávislé směrovače dat", "file-proto-spec": "Přečtěte si specifikaci XFTP protokolu →", - "file-proto-p-2": "Šifrovací klíč souboru je obsažen pouze v části hash adresy URL – váš prohlížeč jej nikdy neodesílá na server. Existují 3 úrovně šifrování: přenos přes protokol TLS, šifrování pro každého příjemce (jedinečný dočasný klíč pro každý přenos) a šifrování souborů typu end-to-end.", + "file-proto-p-2": "Šifrovací klíč souboru je obsažen pouze v části hash adresy URL – váš prohlížeč jej nikdy neodesílá na server. Existují 3 úrovně šifrování: přenos přes protokol TLS, šifrování pro každého příjemce (jedinečný dočasný klíč pro každý přenos) a end-to-end šifrování souborů.", "file-proto-p-4": "Když je soubor rozdělen na části, je odeslán přes síťové směrovače provozované nezávislými stranami. Žádný operátor nemůže vidět aktuální velikost nebo jméno souboru. I kdyby byl směrovač ohrožen, může vidět pouze šifrované části s pevně stanovenou velikosti. Části souboru jsou v mezipaměti síťových směrovačů uchovávány přibližně 48 hodin.", "send-file": "Odeslat soubor" } diff --git a/website/langs/de.json b/website/langs/de.json index 17fb9b184a..cc79255259 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -188,7 +188,7 @@ "copy-the-command-below-text": "Kopieren Sie sich das unten genannte Kommando und nutzen Sie es im Chat:", "privacy-matters-section-subheader": "Die Wahrung der Privatsphäre Ihrer Metadaten — mit wem Sie wann Kontakt haben — schützt Sie vor:", "simplex-private-section-header": "Was macht SimpleX vertraulich", - "simplex-network-section-desc": "Durch die Kombination der Vorteile von P2P und föderierten Netzwerken stellt SimpleX die bestmögliche Privatsphäre zur Verfügung.", + "simplex-network-section-desc": "Durch die Kombination der Vorteile von P2P und föderierten Netzwerken stellt SimpleX Chat die bestmögliche Privatsphäre zur Verfügung.", "simplex-network-2-header": "Im Gegensatz zu föderierten Netzwerken", "comparison-section-list-point-1": "Normalerweise auf der Grundlage einer Telefonnummer, in einigen Fällen auf der Grundlage von Benutzernamen", "comparison-point-5-text": "Zentrale Komponente oder andere Netzwerk-weite Angriffe", @@ -285,15 +285,16 @@ "index-token-p2": "Server werden mit Gutscheinen bezahlt, damit Ihre Communitys kostenlos und unabhängig bleiben können.", "index-token-cta": "Erfahren Sie mehr und sichern Sie sich einen kostenlosen Zugangspass, um es frühzeitig auszuprobieren.", "index-roadmap-h2": "SimpleX - Der Weg zum freien Internet", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Skalierung auf große Communitys", - "index-roadmap-2025-desc": "Ausstieg aus zentralisierten Plattformen", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Nachhaltige Communitys & Server", - "index-roadmap-2026-desc": "Einführung von Community-Gutscheinen", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Lassen Sie Ihre Communitys wachsen", - "index-roadmap-2027-desc": "Tools zur Förderung Ihrer Communitys", + "index-roadmap-now": "Jetzt", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Skalierung auf große Communitys", + "index-roadmap-1-desc": "Ausstieg aus zentralisierten Plattformen", + "index-roadmap-2": "Juni 2027", + "index-roadmap-2-title": "Nachhaltige Communitys & Server", + "index-roadmap-2-desc": "Einführung von Community-Gutscheinen", + "index-roadmap-3": "Dez 2027", + "index-roadmap-3-title": "Lassen Sie Ihre Communitys wachsen", + "index-roadmap-3-desc": "Tools zur Förderung Ihrer Communitys", "index-directory-h2": "Treten Sie SimpleX-Communitys bei", "index-directory-p1": "Hunderttausende Nutzer vertrauen bereits dem Messaging per SimpleX.", "index-directory-p2": "Finden Sie Communitys im SimpleX-Verzeichnis oder erstellen Sie Ihre Eigenen!", @@ -336,7 +337,7 @@ "file-drop-text": "Datei per Drag & Drop hinzufügen", "file-drop-hint": "oder", "file-choose": "Datei auswählen", - "file-max-size": "Max. 100 MB — die SimpleX Chat App unterstützt Dateien bis zu 1 GB", + "file-max-size": "Max. 100 MB — die SimpleX Chat App unterstützt Dateien bis zu 1 GB", "file-encrypting": "Wird verschlüsselt…", "file-uploading": "Wird hochgeladen…", "file-cancel": "Abbrechen", @@ -347,7 +348,7 @@ "file-expiry": "Dateien sind in der Regel 48 Stunden lang verfügbar.", "file-sec-1": "Ihre Datei wurde im Browser verschlüsselt — Datenrouter sehen weder Inhalt, Namen noch Größe der Datei.", "file-sec-2": "Der für die Verschlüsselung genutzte Schlüssel befindet sich im Hash‑Fragment des Links und wird niemals an einen Server übertragen.", - "file-sec-3": "Für noch mehr Sicherheit verwenden Sie die SimpleX Chat App.", + "file-sec-3": "Für noch mehr Sicherheit verwenden Sie die SimpleX Chat App.", "file-retry": "Wiederholen", "file-downloading": "Wird heruntergeladen…", "file-decrypting": "Wird entschlüsselt…", diff --git a/website/langs/en.json b/website/langs/en.json index aea1d057d4..25e5f128ed 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -18,7 +18,7 @@ "simplex-explained-tab-2-p-1": "For each connection you use two separate messaging queues to send and receive messages via different servers.", "simplex-explained-tab-2-p-2": "Servers only pass messages one way, without having the full picture of user's conversations or connections.", "simplex-explained-tab-3-p-1": "The servers have separate anonymous credentials for each queue, and do not know which users they belong to.", - "simplex-explained-tab-3-p-2": "Users can further improve metadata privacy by using Tor to access servers, preventing corellation by IP address.", + "simplex-explained-tab-3-p-2": "Users can further improve metadata privacy by using Tor to access servers, preventing correlation by IP address.", "chat-bot-example": "Chat bot example", "smp-protocol": "SMP protocol", "chat-protocol": "Chat protocol", @@ -76,7 +76,7 @@ "simplex-private-card-9-point-1": "Each message queue passes messages in one direction, with the different send and receive addresses.", "simplex-private-card-9-point-2": "It reduces the attack vectors, compared with traditional message brokers, and available meta-data.", "simplex-private-card-10-point-1": "SimpleX uses temporary anonymous pairwise addresses and credentials for each user contact or group member.", - "simplex-private-card-10-point-2": "It allows to deliver messages without user profile identifiers, providing better meta-data privacy than alternatives.", + "simplex-private-card-10-point-2": "It allows messages to be delivered without user profile identifiers, providing better meta-data privacy than alternatives.", "privacy-matters-1-title": "Advertising and price discrimination", "privacy-matters-1-overlay-1-title": "Privacy saves you money", "privacy-matters-1-overlay-1-linkText": "Privacy saves you money", @@ -113,13 +113,13 @@ "simplex-network-overlay-card-1-li-3": "P2P does not solve MITM attack problem, and most existing implementations do not use out-of-band messages for the initial key exchange. SimpleX uses out-of-band messages or, in some cases, pre-existing secure and trusted connections for the initial key exchange.", "simplex-network-overlay-card-1-li-4": "P2P implementations can be blocked by some Internet providers (like BitTorrent). SimpleX is transport agnostic — it can work over standard web protocols, e.g. WebSockets.", "simplex-network-overlay-card-1-li-5": "All known P2P networks may be vulnerable to Sybil attack, because each node is discoverable, and the network operates as a whole. Known measures to mitigate it require either a centralized component or expensive proof of work. SimpleX network has no server discoverability, it is fragmented and operates as multiple isolated sub-networks, making network-wide attacks impossible.", - "simplex-network-overlay-card-1-li-6": "P2P networks may be vulnerable to DRDoS attack, when the clients can rebroadcast and amplify traffic, resulting in network-wide denial of service. SimpleX clients only relay traffic from known connection and cannot be used by an attacker to amplify the traffic in the whole network.", + "simplex-network-overlay-card-1-li-6": "P2P networks may be vulnerable to DRDoS attack, when the clients can rebroadcast and amplify traffic, resulting in network-wide denial of service. SimpleX clients only relay traffic from known connections and cannot be used by an attacker to amplify the traffic in the whole network.", "privacy-matters-overlay-card-1-p-1": "Many large companies use information about who you are connected with to estimate your income, sell you the products you don't really need, and to determine the prices.", "privacy-matters-overlay-card-1-p-2": "Online retailers know that people with lower incomes are more likely to make urgent purchases, so they may charge higher prices or remove discounts.", "privacy-matters-overlay-card-1-p-3": "Some financial and insurance companies use social graphs to determine interest rates and premiums. It often makes people with lower incomes pay more — it is known as \"poverty premium\".", - "privacy-matters-overlay-card-1-p-4": "SimpleX network protects the privacy of your connections better than any alternative, fully preventing your social graph becoming available to any companies or organizations. Even when people use servers preconfigured in SimpleX Chat apps, server operators do not know the number of users or their connections.", + "privacy-matters-overlay-card-1-p-4": "SimpleX network protects the privacy of your connections better than any alternative, fully preventing your social graph from becoming available to any companies or organizations. Even when people use servers preconfigured in SimpleX Chat apps, server operators do not know the number of users or their connections.", "privacy-matters-overlay-card-2-p-1": "Not so long ago we observed the major elections being manipulated by a reputable consulting company that used our social graphs to distort our view of the real world and manipulate our votes.", - "privacy-matters-overlay-card-2-p-2": "To be objective and to make independent decisions you need to be in control of your information space. It is only possible if you use private communication network that does not have access to your social graph.", + "privacy-matters-overlay-card-2-p-2": "To be objective and to make independent decisions you need to be in control of your information space. It is only possible if you use a private communication network that does not have access to your social graph.", "privacy-matters-overlay-card-2-p-3": "SimpleX is the first network that doesn't have any user identifiers by design, in this way protecting your connections graph better than any known alternative.", "privacy-matters-overlay-card-3-p-1": "Everyone should care about privacy and security of their communications — harmless conversations can put you in danger, even if you have nothing to hide.", "privacy-matters-overlay-card-3-p-2": "One of the most shocking stories is the experience of Mohamedou Ould Salahi described in his memoir and shown in The Mauritanian movie. He was put into Guantanamo camp, without trial, and was tortured there for 15 years after a phone call to his relative in Afghanistan, under suspicion of being involved in 9/11 attacks, even though he lived in Germany for the previous 10 years.", @@ -135,7 +135,7 @@ "simplex-unique-overlay-card-3-p-3": "Unlike federated networks servers (email, XMPP or Matrix), SimpleX servers don't store user accounts, they only relay messages, protecting the privacy of both parties.", "simplex-unique-overlay-card-3-p-4": "There are no identifiers or ciphertext in common between sent and received server traffic — if anybody is observing it, they cannot easily determine who communicates with whom, even if TLS is compromised.", "simplex-unique-overlay-card-4-p-1": "You can use SimpleX with your own servers and still communicate with people who use the servers preconfigured in the apps.", - "simplex-unique-overlay-card-4-p-2": "SimpleX network uses an open protocol and provides SDK to create chat bots, allowing implementation of services that users can interact with via SimpleX Chat apps — we're really looking forward to see what SimpleX services you will build.", + "simplex-unique-overlay-card-4-p-2": "SimpleX network uses an open protocol and provides SDK to create chat bots, allowing implementation of services that users can interact with via SimpleX Chat apps — we're really looking forward to seeing what SimpleX services you will build.", "simplex-unique-overlay-card-4-p-3": "If you are considering developing for the SimpleX network, for example, the chat bot for SimpleX app users, or the integration of the SimpleX Chat library into your mobile apps, please get in touch for any advice and support.", "simplex-unique-card-1-p-1": "SimpleX protects the privacy of your profile, contacts and metadata, hiding it from SimpleX network servers and any observers.", "simplex-unique-card-1-p-2": "Unlike any other existing messaging network, SimpleX has no identifiers assigned to the users — not even random numbers.", @@ -166,7 +166,7 @@ "to-make-a-connection": "To make a connection:", "install-simplex-app": "Install SimpleX app", "connect-in-app": "Connect in app", - "open-simplex-app": "Open Simplex app", + "open-simplex-app": "Open SimpleX app", "tap-the-connect-button-in-the-app": "Tap the \"connect\" button in the app", "scan-the-qr-code-with-the-simplex-chat-app": "Scan the QR code with the SimpleX Chat app", "scan-the-qr-code-with-the-simplex-chat-app-description": "The public keys and message queue address in this link are NOT sent over the network when you view this page —
they are contained in the hash fragment of the link URL.", @@ -287,15 +287,16 @@ "index-token-p2": "Vouchers will pay for servers, to let your communities stay free and independent.", "index-token-cta": "Learn more and get a free access pass for early testing.", "index-roadmap-h2": "SimpleX Roadmap to Free Internet", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Scale to Large Communities", - "index-roadmap-2025-desc": "Escaping centralized platforms", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Sustainable Communities & Servers", - "index-roadmap-2026-desc": "Launching Community Vouchers", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Make Your Communities Grow", - "index-roadmap-2027-desc": "Tools to promote your communities", + "index-roadmap-now": "Now", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Scale to Large Communities", + "index-roadmap-1-desc": "Escaping centralized platforms", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Sustainable Communities & Servers", + "index-roadmap-2-desc": "Launching Community Vouchers", + "index-roadmap-3": "Dec 2027", + "index-roadmap-3-title": "Make Your Communities Grow", + "index-roadmap-3-desc": "Tools to promote your communities", "index-directory-h2": "Join SimpleX Communities", "index-directory-p1": "Hundreds of thousands people already trust SimpleX messaging.", "index-directory-p2": "Find your communities in SimpleX directory and create your own!", diff --git a/website/langs/es.json b/website/langs/es.json index 5c708ef681..421cdbe9bd 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -285,15 +285,16 @@ "index-token-p2": "Los vales costearán los servidores para que tus comunidades sigan siendo libres e independientes.", "index-token-cta": "Descubre más y obtén acceso gratuito para participar en las pruebas iniciales.", "index-roadmap-h2": "Ruta SimpleX hacía el Internet Libre", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Escalar a Comunidades Grandes", - "index-roadmap-2025-desc": "Huir de plataformas centralizadas", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Comunidades y Servidores Sostenibles", - "index-roadmap-2026-desc": "Lanzamiento de Vales Comunitarios", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Haz Crecer Tus Comunidades", - "index-roadmap-2027-desc": "Herramientas para la promoción", + "index-roadmap-now": "Ahora", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Escalar a Comunidades Grandes", + "index-roadmap-1-desc": "Huir de plataformas centralizadas", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Comunidades y Servidores Sostenibles", + "index-roadmap-2-desc": "Lanzamiento de Vales Comunitarios", + "index-roadmap-3": "Dic 2027", + "index-roadmap-3-title": "Haz Crecer Tus Comunidades", + "index-roadmap-3-desc": "Herramientas para la promoción", "index-directory-h2": "Únete a las Comunidades SimpleX", "index-directory-p1": "Miles de personas confían ya en la mensajería SimpleX.", "index-directory-p2": "¡Encuentra comunidades en el directorio SimpleX y crea la tuya!", diff --git a/website/langs/fr.json b/website/langs/fr.json index b2551990fd..91ffa8e408 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -280,6 +280,15 @@ "index-token-h2": "Des communautés qui durent", "index-token-cta": "En savoir plus et obtenez votre NFT gratuit
pour les premiers tests.", "index-roadmap-h2": "Feuille de route de SimpleX vers un Internet libre", - "index-roadmap-2025": "2025", + "index-roadmap-now": "Maintenant", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Mise à l'échelle pour les grandes communautés", + "index-roadmap-1-desc": "Quitter les plateformes centralisées", + "index-roadmap-2": "Juin 2027", + "index-roadmap-2-title": "Communautés et serveurs durables", + "index-roadmap-2-desc": "Lancement des bons communautaires", + "index-roadmap-3": "Déc 2027", + "index-roadmap-3-title": "Faites grandir vos communautés", + "index-roadmap-3-desc": "Outils pour promouvoir vos communautés", "send-file": "Envoyer un fichier" } diff --git a/website/langs/hu.json b/website/langs/hu.json index 5dda790d96..da5de1128d 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -21,7 +21,7 @@ "chat-protocol": "Csevegési protokoll", "donate": "Adományozás", "copyright-label": "© 2020-2025 SimpleX Chat | Nyílt forráskódú projekt", - "simplex-chat-protocol": "A SimpleX Chat protokoll", + "simplex-chat-protocol": "SimpleX Chat protokoll", "terminal-cli": "Terminál CLI", "terms-and-privacy-policy": "Adatvédelmi irányelvek", "hero-header": "Újraértelmezett adatvédelem", @@ -49,7 +49,7 @@ "simplex-private-4-title": "Hozzáférés a Tor hálózaton keresztül
(nem kötelező)", "simplex-private-5-title": "Többrétegű
tartalomkitöltés", "simplex-private-6-title": "Sávon kívüli
kulcscsere", - "simplex-private-7-title": "Üzenetintegritás
hitelesítés", + "simplex-private-7-title": "Üzenetintegritás
ellenőrzés", "simplex-private-8-title": "Üzenetek keverése
a korreláció csökkentése érdekében", "simplex-private-9-title": "Egyirányú
várólista az üzenetekhez", "simplex-private-10-title": "Ideiglenes, névtelen, páronkénti azonosítók", @@ -118,7 +118,7 @@ "simplex-unique-overlay-card-2-p-1": "Mivel senki sem rendelkezik azonosítóval a SimpleX hálózaton, ezért senki sem tud kapcsolatba lépni Önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-overlay-card-2-p-2": "Még a felhasználói cím használata esetén is, aminek használata nem kötelező – ugyanakkor ez a kéretlen kapcsolatkérelmek küldésére is használható – módosíthatja vagy teljesen törölheti a címet anélkül, hogy elveszítené a meglévő kapcsolatait.", "simplex-unique-overlay-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban, amely exportálható és átvihető bármely más támogatott eszközre.", - "simplex-unique-overlay-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tárolódnak, amíg meg nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", + "simplex-unique-overlay-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX átjátszóin tárolódnak, amíg meg nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", "simplex-unique-overlay-card-3-p-3": "A föderált hálózatok kiszolgálóitól (e-mail, XMPP vagy Matrix) eltérően a SimpleX kiszolgálók nem tárolják a felhasználói fiókokat, csak továbbítják az üzeneteket, így védve mindkét fél magánéletét.", "simplex-unique-overlay-card-3-p-4": "A küldött és a fogadott kiszolgálóforgalom között nincsenek közös azonosítók vagy titkosított szövegek — ha bárki megfigyeli, nem tudja könnyen megállapítani, hogy ki kivel kommunikál, még akkor sem, ha a TLS-t kompromittálják.", "simplex-unique-overlay-card-4-p-1": "Használhatja a SimpleXet a saját kiszolgálóival, és továbbra is kommunikálhat azokkal, akik az előre beállított kiszolgálókat használják az alkalmazásban.", @@ -128,7 +128,7 @@ "simplex-unique-card-1-p-2": "Minden más létező üzenetküldő hálózattól eltérően a SimpleX nem rendelkezik a felhasználókhoz rendelt azonosítókkal — még véletlenszerű számokkal sem.", "simplex-unique-card-2-p-1": "Mivel a SimpleX hálózaton senkinek sincs azonosítója vagy állandó címe, ezért senki sem tud kapcsolatba lépni a felhasználókkal, hacsak nem osztanak meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban —, amely exportálható és átvihető bármely más támogatott eszközre.", - "simplex-unique-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", + "simplex-unique-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX átjátszóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", "simplex-unique-card-4-p-1": "A SimpleX hálózat teljesen decentralizált és független bármely kriptopénztől vagy bármely más hálózattól, kivéve az internetet.", "simplex-unique-card-4-p-2": "Használhatja a SimpleXet a saját kiszolgálóival vagy az általunk biztosított kiszolgálókkal, és továbbra is kapcsolódhat bármely felhasználóhoz.", "join": "Csatlakozzon a közösségeinkhez", @@ -143,17 +143,17 @@ "learn-more": "Tudjon meg többet", "more-info": "További információ", "hide-info": "Információ elrejtése", - "contact-hero-subheader": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással a telefonjával vagy táblagépével.", - "contact-hero-p-1": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várólistájának címe NEM kerül elküldésre a hálózaton keresztül, amikor megtekinti ezt az oldalt — azokat, a hivatkozás webcímének kivonattöredéke tartalmazza.", + "contact-hero-subheader": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással az eszköze segítségével.", + "contact-hero-p-1": "A hivatkozásban szereplő nyilvános kulcsok és az üzenetek várólistájának címe NEM lesz elküldve a hálózaton keresztül, amikor megtekinti ezt az oldalt — azokat, a hivatkozás webcímének kivonattöredéke tartalmazza.", "contact-hero-p-2": "Még nem töltötte le a SimpleX Chatet?", - "contact-hero-p-3": "Az alkalmazás letöltéséhez használja az alábbi hivatkozásokat.", + "contact-hero-p-3": "Az alkalmazás letöltéséhez használja az alábbi hivatkozások egyikét.", "scan-qr-code-from-mobile-app": "QR-kód beolvasása mobilalkalmazásból", "to-make-a-connection": "A kapcsolat létrehozásához:", "install-simplex-app": "Telepítse a SimpleX alkalmazást", "open-simplex-app": "Simplex alkalmazás megnyitása", "tap-the-connect-button-in-the-app": "Koppintson a „kapcsolódás” gombra az alkalmazásban", "scan-the-qr-code-with-the-simplex-chat-app": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással", - "scan-the-qr-code-with-the-simplex-chat-app-description": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várólistájának címe NEM kerül elküldésre a hálózaton keresztül, amikor ezt az oldalt megtekinti —
ezek a hivatkozás webcímének kivonattöredékében szerepelnek.", + "scan-the-qr-code-with-the-simplex-chat-app-description": "A hivatkozásban szereplő nyilvános kulcsok és az üzenetek várólistájának címe NEM lesz elküldve a hálózaton keresztül, amikor megtekinti ezt az oldalt —
azokat, a hivatkozás webcímének kivonattöredéke tartalmazza.", "installing-simplex-chat-to-terminal": "A SimpleX chat telepítése a terminálhoz", "use-this-command": "Használja ezt a parancsot:", "see-simplex-chat": "Az utasításokat megtekintheti a SimpleX Chat", @@ -171,7 +171,7 @@ "simplex-network-section-desc": "A SimpleX Chat a P2P- és a föderált hálózatok előnyeinek kombinálásával biztosítja a legjobb adatvédelmet.", "simplex-network-1-desc": "Minden üzenet a kiszolgálókon keresztül kerül elküldésre, ami jobb metaadat-védelmet és megbízható aszinkron üzenetkézbesítést biztosít, miközben elkerülhető a sok", "simplex-network-2-header": "A föderált hálózatokkal ellentétben", - "simplex-network-2-desc": "A SimpleX továbbítókiszolgálói NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM kapcsolódnak egymáshoz, és NINCS kiszolgálójegyzék.", + "simplex-network-2-desc": "A SimpleX átjátszó kiszolgálói NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM kapcsolódnak egymáshoz, és NINCS kiszolgálójegyzék.", "simplex-network-3-header": "SimpleX hálózat", "simplex-network-3-desc": "a kiszolgálók egyirányú várólistákat biztosítanak a felhasználók összekapcsolásához, de nem látják a hálózati kapcsolati gráfot; azt csak a felhasználók látják.", "comparison-section-header": "Összehasonlítás más protokollokkal", @@ -191,13 +191,13 @@ "comparison-section-list-point-1": "Általában telefonszám alapján, néhány esetben felhasználónév alapján", "comparison-section-list-point-2": "DNS-alapú címek", "comparison-section-list-point-3": "Nyilvános kulcs vagy más globális egyedi azonosító", - "comparison-section-list-point-4a": "A SimpleX továbbítókiszolgálói nem veszélyeztethetik a végpontok közötti titkosítást. Hitelesítse a biztonsági kódot a sávon kívüli csatorna elleni támadások veszélyeinek csökkentésére", - "comparison-section-list-point-4": "Ha az üzemeltetett kiszolgálók veszélybe kerülnek. Hitelesítse a biztonsági kódot a Signal vagy más biztonságos üzenetküldő alkalmazás segítségével a támadások veszélyeinek csökkentésére", + "comparison-section-list-point-4a": "A SimpleX átjátszói nem veszélyeztethetik a végpontok közötti titkosítást. Ellenőrizze a biztonsági kódot a sávon kívüli csatorna elleni támadások veszélyeinek csökkentésére", + "comparison-section-list-point-4": "Ha az üzemeltetett kiszolgálók veszélybe kerülnek. Ellenőrizze a biztonsági kódot a Signal vagy más biztonságos üzenetküldő alkalmazás segítségével a támadások veszélyeinek csökkentésére", "comparison-section-list-point-5": "Nem védi a felhasználók metaadatait", "comparison-section-list-point-6": "Bár a P2P elosztott, de nem föderált — egyetlen hálózatként működnek", "comparison-section-list-point-7": "A P2P-hálózatoknak vagy van egy központi hitelesítője, vagy az egész hálózat kompromittálódhat", "see-here": "tekintse meg itt", - "guide-dropdown-1": "Gyors indítás", + "guide-dropdown-1": "Gyorsindítás", "guide-dropdown-2": "Üzenetek küldése", "guide-dropdown-3": "Titkos csoportok", "guide-dropdown-4": "Csevegési profilok", @@ -209,7 +209,7 @@ "docs-dropdown-1": "SimpleX hálózat", "docs-dropdown-2": "Android fájlok elérése", "docs-dropdown-3": "Hozzáférés a csevegési adatbázishoz", - "docs-dropdown-8": "SimpleX csoportjegyzék", + "docs-dropdown-8": "SimpleX-csoportjegyzék", "docs-dropdown-9": "Letöltések", "f-droid-page-simplex-chat-repo-section-text": "Ha hozzá szeretné adni az F-Droid klienséhez, olvassa be a QR-kódot, vagy használja ezt a webcímet:", "signing-key-fingerprint": "Az aláírókulcs ujjlenyomata (SHA-256)", @@ -219,8 +219,8 @@ "f-droid-page-f-droid-org-repo-section-text": "A SimpleX Chat és az F-Droid-tároló különböző kulcsokkal írják alá az összeállításokat. A váltáshoz exportálja a csevegési adatbázist és telepítse újra az alkalmazást.", "jobs": "Csatlakozzon a csapathoz", "please-enable-javascript": "Engedélyezze a JavaScriptet a QR-kód megjelenítéséhez.", - "please-use-link-in-mobile-app": "Használja a mobilalkalmazásban található hivatkozást", - "contact-hero-header": "Kapott egy meghívót a SimpleX Chaten való beszélgetéshez", + "please-use-link-in-mobile-app": "Használja a hivatkozást a SimpleX Chat alkalmazásban", + "contact-hero-header": "Meghívót kapott a SimpleX Chaten való beszélgetéshez", "invitation-hero-header": "Kapott egy egyszer használható meghívót a SimpleX Chaten való beszélgetéshez", "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független — a szabványos webes protokollokon, például WebSocketsen keresztül is működik.", "simplex-private-card-4-point-2": "A SimpleX, Tor hálózaton keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", @@ -266,7 +266,7 @@ "index-security-review-2024-title": "Biztonsági audit 2024", "index-security-audits-label": "Biztonsági
auditok", "index-publications-heise-title": "A Heise Online kiadványai", - "index-hero-h2": "Az Ön hálózatában", + "index-hero-h2": "A saját hálózatában", "index-testflight-title": "Nyilvános betekintés az iOS alkalmazás fejlesztésébe a TestFlighton", "index-f-droid-title": "SimpleX alkalmazás az F-Droidon keresztül", "index-publications-privacy-guides-title": "A Privacy Guides üzenetváltó ajánlásai", @@ -279,25 +279,26 @@ "index-messaging-cta": "Tudjon meg többet a SimpleX üzenetváltó alkalmazásról", "index-nextweb-h2": "Vegye birtokba
A jövő hálózatát", "index-nextweb-p1": "A SimpleX abból a meggyőződésből jött létre, hogy a profilok, a kapcsolatok és a közösségek a felhasználók tulajdonát kell, hogy képezzék.", - "index-nextweb-p2": "Egy decentralizált hálózat, amelyet senki sem birtokol, lehetővé teszi a kapcsolatok létrehozását és az ötleteket megosztását szabadon és biztonságosan a hálózaton.", + "index-nextweb-p2": "Egy decentralizált hálózat, amelyet senki sem birtokol, lehetővé teszi a kapcsolatok létrehozását és az ötletek megosztását szabadon és biztonságosan a hálózaton.", "index-token-h2": "Időtálló közösségek", "index-token-p1": "A jövőben közösségi utalványokkal támogathatja a kedvenc csoportjait.", "index-token-p2": "Az utalványokkal fizetni tudja a kiszolgálókat, hogy a közösségek szabadok és függetlenek maradhassanak.", "index-token-cta": "Tudjon meg többet, és szerezzen ingyenes NFT-t az előzetes tesztelésért.", "index-roadmap-h2": "A SimpleX ütemterve a szabad internethez", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Skálázódás nagy közösségekre", - "index-roadmap-2025-desc": "Központosított platformok elhagyása", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Fenntartható közösségek és kiszolgálók", - "index-roadmap-2026-desc": "Közösségi utalványok elindítása", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Növelje közösségeit", - "index-roadmap-2027-desc": "Eszközök a közösségei népszerűsítéséhez", + "index-roadmap-now": "Most", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Skálázódás nagy közösségekre", + "index-roadmap-1-desc": "Központosított platformok elhagyása", + "index-roadmap-2": "2027. jún.", + "index-roadmap-2-title": "Fenntartható közösségek és kiszolgálók", + "index-roadmap-2-desc": "Közösségi utalványok elindítása", + "index-roadmap-3": "2027. dec.", + "index-roadmap-3-title": "Közösségek növelése", + "index-roadmap-3-desc": "Eszközök biztosítása a közösségek népszerűsítéséhez", "index-directory-h2": "Csatlakozzon a SimpleX közösségekhez", "index-directory-p1": "Már emberek százezrei bíznak a SimpleXen való üzenetváltásban.", - "index-directory-p2": "Találja meg a kedvenc közösségeit a SimpleX csoportjegyzékében, vagy hozza létre a saját csoportját!", - "index-directory-cta": "SimpleX csoportjegyzék megtekintése", + "index-directory-p2": "Találja meg a kedvenc közösségeit a SimpleX-csoportjegyzékben vagy hozza létre a saját csoportját!", + "index-directory-cta": "SimpleX-csoportjegyzék megtekintése", "index-directory-users-group-title": "SimpleX felhasználók csoportja", "how-secure-comparison-title": "A végpontok közötti titkosítás összehasonlítása más üzenetváltó alkalmazásokkal", "how-secure-message-padding": "Tartalomkitöltés", @@ -315,13 +316,13 @@ "navbar-token": "Token", "navbar-old-site": "Régi oldal", "docs-dropdown-15": "Összeállítások ellenőrzése és reprodukálása", - "why-p2": "Senki sem követi nyomon a beszélgetéseit. Senki sem készít térképet az Ön kapcsolati hálójáról. A magánélet nem csak egy funkció, hanem egy életmód.", - "why-p3": "Amikor online vagyunk minden platform egy darabot kér tőlünk – nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött – telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód.", + "why-p2": "Senki sem követte nyomon a beszélgetéseit. Senki sem készített térképet arról, hogy merre járt. A magánélet nem csak egy funkció volt, hanem egy életmód.", + "why-p3": "Aztán felléptünk az internetre, és minden platform kért belőlünk egy darabot — nevet, telefonszámot, baráti kapcsolatokat. Elfogadtuk, hogy a kommunikáció ára az, hogy mások megtudják, hogy kivel beszélünk. Minden generáció, az emberek és a technológia is eddig így működött — telefon, e-mail, üzenetküldő programok, közösségi média. Úgy tűnt, ez az egyetlen lehetséges mód.", "why-p4": "De van egy másik lehetőség is. Egy hálózat, amelyben nincsenek telefonszámok. Nincsenek felhasználónevek. Nincsenek fiókok. Nincsenek semmiféle felhasználói azonosítók. Egy hálózat, amely összeköti az embereket és titkosított üzeneteket továbbít, anélkül, hogy tudná, ki csatlakozik hozzá.", - "why-p5": "Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Egyetlen „kíváncsiskodó” sem tekinthet bele a beszélgetéseibe, Ön itt szuverén.", + "why-p5": "Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide — Ön itt szuverén.", "why-p6": "A beszélgetései Önhöz tartoznak, ahogy az internet megjelenése előtt is mindig így volt. A hálózat nem egy hely, amelyet meglátogat. Ez egy olyan hely, amelyet Ön hoz létre saját magának. És senki sem veheti el Öntől, függetlenül attól, hogy privát vagy nyilvános.", - "why-p7": "A legrégebbi emberi szabadság – beszélgetni az emberekkel anélkül, hogy mások megfigyelnének – olyan infrastruktúrán alapul, amely nem tudja elárulni.", - "why-p8": "Mert elpusztítottuk azt az erőt, amellyel megtudhatnánk, hogy Ön kicsoda. Hogy az Ön ereje soha ne kerülhessen mások kezébe.", + "why-p7": "A legrégebbi emberi szabadság — beszélgetni az emberekkel, anélkül, hogy mások megfigyelnének — olyan infrastruktúrán alapul, amely nem tudja elárulni.", + "why-p8": "Mert felszámoltuk a lehetőségét is annak, hogy megtudjuk, Ön kicsoda. Így az önrendelkezése soha nem kerülhet idegen kezekbe.", "why-tagline": "Legyen szabad a saját hálózatában.", "why-footer-link": "Miért készítjük", "why-p1": "Ön fiók nélkül született.", @@ -335,8 +336,8 @@ "file-title": "SimpleX fájlátvitel", "file-drop-text": "Húzzon ide", "file-drop-hint": "vagy", - "file-choose": "válasszon ki egy fájlt", - "file-max-size": "Legfeljebb 100 MB – a SimpleX Chat alkalmazás viszont 1 GB méretű fájlokat is támogat", + "file-choose": "Válasszon ki egy fájlt", + "file-max-size": "Legfeljebb 100 MB – a SimpleX Chat alkalmazás viszont 1 GB méretű fájlokat is támogat", "file-encrypting": "Titkosítás…", "file-uploading": "Feltöltés…", "file-cancel": "Mégse", @@ -347,7 +348,7 @@ "file-expiry": "A fájlok általában 48 óráig érhetők el.", "file-sec-1": "A fájl a böngészőben lett titkosítva – az útválasztók soha nem „látják” a fájl tartalmát, nevét és méretét.", "file-sec-2": "A titkosítási kulcs a hivatkozás kivonattöredékében található – soha nem kerül elküldésre semmilyen kiszolgálóra.", - "file-sec-3": "A nagyobb biztonság érdekében használja a SimpleX Chat alkalmazást.", + "file-sec-3": "A nagyobb biztonság érdekében használja a SimpleX Chat alkalmazást.", "file-retry": "Újra", "file-downloading": "Letöltés…", "file-decrypting": "Visszafejtés…", diff --git a/website/langs/id.json b/website/langs/id.json index 614f5937c8..3768926f9d 100644 --- a/website/langs/id.json +++ b/website/langs/id.json @@ -285,15 +285,16 @@ "index-token-p2": "Voucher akan membayar server, agar komunitas Anda tetap bebas dan independen.", "index-token-cta": "Pelajari lebih lanjut tentang Voucher Komunitas", "index-roadmap-h2": "SimpleX Roadmap Menuju Internet Bebas", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Skala ke Komunitas Besar", - "index-roadmap-2025-desc": "Menghindari platform terpusat", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Komunitas & Server Berkelanjutan", - "index-roadmap-2026-desc": "Perilisan Voucher Komunitas", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Buat Komunitas Anda Meningkat", - "index-roadmap-2027-desc": "Alat untuk promosikan komunitas Anda", + "index-roadmap-now": "Sekarang", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Skala ke Komunitas Besar", + "index-roadmap-1-desc": "Menghindari platform terpusat", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Komunitas & Server Berkelanjutan", + "index-roadmap-2-desc": "Perilisan Voucher Komunitas", + "index-roadmap-3": "Des 2027", + "index-roadmap-3-title": "Buat Komunitas Anda Meningkat", + "index-roadmap-3-desc": "Alat untuk promosikan komunitas Anda", "index-directory-h2": "Gabung ke Komunitas SimpleX", "index-directory-p1": "Ratusan ribu orang sudah memercayai perpesanan SimpleX.", "index-directory-p2": "Temukan komunitas Anda di direktori SimpleX dan buat komunitas Anda sendiri!", diff --git a/website/langs/it.json b/website/langs/it.json index c7f6679a74..0a6870d75d 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -285,15 +285,16 @@ "index-token-p2": "I buoni pagheranno i server, per consentire alle tue comunità di rimanere libere e indipendenti.", "index-token-cta": "Scopri di più e ricevi un pass di accesso gratuito per provarlo in anticipo.", "index-roadmap-h2": "Tabella di marcia per un internet libero", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Scalabilità per comunità numerose", - "index-roadmap-2025-desc": "Fuga da piattaforme centralizzate", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Comunità e server sostenibili", - "index-roadmap-2026-desc": "Pubblicazione di buoni comunitari", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Fare crescere le tue comunità", - "index-roadmap-2027-desc": "Strumenti per promuovere le tue comunità", + "index-roadmap-now": "Ora", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Scalabilità per comunità numerose", + "index-roadmap-1-desc": "Fuga da piattaforme centralizzate", + "index-roadmap-2": "Giu 2027", + "index-roadmap-2-title": "Comunità e server sostenibili", + "index-roadmap-2-desc": "Pubblicazione di buoni comunitari", + "index-roadmap-3": "Dic 2027", + "index-roadmap-3-title": "Fare crescere le tue comunità", + "index-roadmap-3-desc": "Strumenti per promuovere le tue comunità", "index-directory-h2": "Unisciti alle comunità di SimpleX", "index-directory-p1": "Centinaia di migliaia di persone si fidano già di SimpleX.", "index-directory-p2": "Trova le comunità nella directory di SimpleX e creane una tua!", @@ -315,14 +316,14 @@ "navbar-token": "Token", "navbar-old-site": "Sito vecchio", "docs-dropdown-15": "Verifica e riproduci le build", - "why-p2": "Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai una caratteristica, era uno stile di vita.", - "why-p3": "Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia le persone che la tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unica via possibile.", + "why-p2": "Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita.", + "why-p3": "Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unico modo possibile.", "why-p1": "Sei nato senza un account.", - "why-p4": "C'è un altro modo. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso.", + "why-p4": "C'è un'altra via. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso.", "why-p5": "Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano.", - "why-p6": "Le tue conversazioni appartengono a te, come è sempre stato prima dell'avvento di Internet. La rete non è un luogo che visiti. È un luogo che crei e possiedi. E nessuno può portartelo via, sia che tu lo renda privato o pubblico.", + "why-p6": "Le tue conversazioni appartengono a te, come è sempre stato prima dell'avvento di internet. La rete non è un luogo che visiti. È un luogo che crei e possiedi. E nessuno può portartelo via, che tu lo renda privato o pubblico.", "why-p7": "La più antica libertà umana, parlare con un'altra persona senza essere osservati, si basa su un'infrastruttura che non può tradirla.", - "why-p8": "Perché abbiamo distrutto il potere di sapere chi sei. In modo che il tuo potere non possa mai essere sottratto.", + "why-p8": "Perché abbiamo distrutto il potere di sapere chi sei. In modo che il tuo potere non possa mai esserti sottratto.", "why-tagline": "Vivi libero nella tua rete.", "why-footer-link": "Perché lo stiamo costruendo", "file": "File", @@ -336,7 +337,7 @@ "file-drop-text": "Trascina un file qui", "file-drop-hint": "o", "file-choose": "Scegli file", - "file-max-size": "Max 100 MB - L'app SimpleX Chat supporta file fino a 1 GB", + "file-max-size": "Max 100 MB - L'app SimpleX Chat supporta file fino a 1 GB", "file-encrypting": "Crittografia…", "file-uploading": "Caricamento…", "file-cancel": "Annulla", @@ -347,7 +348,7 @@ "file-expiry": "I file sono generalmente disponibili per 48 ore.", "file-sec-1": "Il file è stato crittografato nel browser: gli instradatori di dati non vedono mai il contenuto, il nome o la dimensione del file.", "file-sec-2": "La chiave di crittografia è nel frammento hash del link, non viene mai inviata ad alcun server.", - "file-sec-3": "Per una migliore sicurezza, usa l'app SimpleX Chat.", + "file-sec-3": "Per una migliore sicurezza, usa l'app SimpleX Chat.", "file-retry": "Riprova", "file-downloading": "Scaricamento…", "file-decrypting": "Decifrazione…", diff --git a/website/langs/ja.json b/website/langs/ja.json index 2337ed472d..aff6d62c1a 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -277,15 +277,16 @@ "index-token-p1": "コミュニティバウチャーを通じて、お気に入りのグループをサポートできます。", "index-token-p2": "バウチャーはサーバー費用に充てられ、コミュニティが自由で独立した状態を保ち続けられるようにします。", "index-roadmap-h2": "自由なインターネットを目指す SimpleX ロードマップ", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "大規模コミュニティへの拡張", - "index-roadmap-2025-desc": "中央集権型プラットフォームからの脱却", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "サステナブルなコミュニティ&サーバ", - "index-roadmap-2026-desc": "コミュニティバウチャーの開始", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "コミュニティの成長", - "index-roadmap-2027-desc": "コミュニティを広げるツール", + "index-roadmap-now": "現在", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "大規模コミュニティへの拡張", + "index-roadmap-1-desc": "中央集権型プラットフォームからの脱却", + "index-roadmap-2": "2027年6月", + "index-roadmap-2-title": "サステナブルなコミュニティ&サーバ", + "index-roadmap-2-desc": "コミュニティバウチャーの開始", + "index-roadmap-3": "2027年12月", + "index-roadmap-3-title": "コミュニティの成長", + "index-roadmap-3-desc": "コミュニティを広げるツール", "index-directory-h2": "SimpleXコミュニティに参加する", "index-directory-p1": "既に何十万人もの人々がSimpleXメッセージングを信頼しています。", "index-directory-p2": "SimpleXディレクトリでコミュニティを見つけたり、あなた自身のコミュニティを作成しましょう!", @@ -296,5 +297,6 @@ "index-publications-heise-title": "Heise Online の記事", "index-publications-kuketz-title": "Mike Kuketzによるレビュー", "index-publications-optout-title": "OptOut ポッドキャストインタビュー", - "send-file": "ファイルを送信" + "send-file": "ファイルを送信", + "navbar-old-site": "旧サイト" } diff --git a/website/langs/pl.json b/website/langs/pl.json index 37582f80e3..09062757b6 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -286,15 +286,16 @@ "index-token-p2": "Vouchery opłacą serwery, aby Twoje społeczności pozostały wolne i niezależne.", "index-token-cta": "Dowiedz się więcej i uzyskaj bezpłatną przepustkę umożliwiającą wczesne testowanie.", "index-roadmap-h2": "Plan Działania SimpleX dla Wolnego Internetu", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Wyskalowany dla Dużych Społeczności", - "index-roadmap-2025-desc": "Wymyka się scentralizowanym platformom", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Zrównoważone Społeczności i Serwery", - "index-roadmap-2026-desc": "Uruchomienie Voucherów Społeczności", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Spraw, aby Twoje społeczności Rosły", - "index-roadmap-2027-desc": "Narzędzia do promowania Twoich społeczności", + "index-roadmap-now": "Teraz", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Wyskalowany dla Dużych Społeczności", + "index-roadmap-1-desc": "Wymyka się scentralizowanym platformom", + "index-roadmap-2": "Cze 2027", + "index-roadmap-2-title": "Zrównoważone Społeczności i Serwery", + "index-roadmap-2-desc": "Uruchomienie Voucherów Społeczności", + "index-roadmap-3": "Gru 2027", + "index-roadmap-3-title": "Spraw, aby Twoje społeczności Rosły", + "index-roadmap-3-desc": "Narzędzia do promowania Twoich społeczności", "index-directory-h2": "Dołącz do Społeczności SimpleX", "index-directory-p1": "Setki tysięcy ludzi już ufają wiadomościom SimpleX.", "index-directory-p2": "Znajdź swoje społeczności w katalogu SimpleX i stwórz własne!", @@ -336,7 +337,7 @@ "file-drop-text": "Przeciągnij i upuść plik tutaj", "file-drop-hint": "lub", "file-choose": "Wybierz plik", - "file-max-size": "Maksymalnie 100 MB - aplikacja SimpleX Chat obsługuje pliki o rozmiarze do 1 GB", + "file-max-size": "Maksymalnie 100 MB - aplikacja SimpleX Chat obsługuje pliki o rozmiarze do 1 GB", "file-encrypting": "Szyfrowanie…", "file-uploading": "Wysyłanie…", "file-cancel": "Anuluj", diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index a94670e443..0fdf72adf0 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -286,15 +286,16 @@ "index-token-p2": "Os vouchers pagarão pelos servidores, permitindo que suas comunidades continuem gratuitas e independentes.", "index-token-cta": "Saiba mais e pegue sua NFT gratuita
para testes antecipados.", "index-roadmap-h2": "Roteiro do SimpleX para uma Internet Livre", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Escala para Grandes Comunidades", - "index-roadmap-2025-desc": "Fugindo de plataformas centralizadas", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Comunidades e Servidores Sustentáveis", - "index-roadmap-2026-desc": "Lançamento dos Vouchers da Comunidade", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Faça Suas Comunidades Crescerem", - "index-roadmap-2027-desc": "Ferramentas para promover suas comunidades", + "index-roadmap-now": "Agora", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Escala para Grandes Comunidades", + "index-roadmap-1-desc": "Fugindo de plataformas centralizadas", + "index-roadmap-2": "Jun 2027", + "index-roadmap-2-title": "Comunidades e Servidores Sustentáveis", + "index-roadmap-2-desc": "Lançamento dos Vouchers da Comunidade", + "index-roadmap-3": "Dez 2027", + "index-roadmap-3-title": "Faça Suas Comunidades Crescerem", + "index-roadmap-3-desc": "Ferramentas para promover suas comunidades", "index-directory-h2": "Participe das Comunidades SimpleX", "index-directory-p1": "Centenas de milhares de pessoas já confiam no SimpleX Messaging.", "index-directory-p2": "Encontre suas comunidades no diretório SimpleX e crie a sua própria!", diff --git a/website/langs/ru.json b/website/langs/ru.json index 235492291f..656c751636 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -285,15 +285,16 @@ "index-token-p2": "Ваучеры будут использоваться для оплаты за серверы, чтобы группы оставались свободными и независимыми.", "index-token-cta": "Узнайте больше и возьмите бесплатный пропуск, чтобы участвовать в тестировании.", "index-roadmap-h2": "Путь Сети SimpleX к Свободному Интернету", - "index-roadmap-2025": "2025", - "index-roadmap-2025-title": "Большие каналы и группы", - "index-roadmap-2025-desc": "Чтобы Вы могли покинуть централизованные платформы", - "index-roadmap-2026": "2026", - "index-roadmap-2026-title": "Самодостаточные группы и серверы", - "index-roadmap-2026-desc": "Запуск Ваучеров Групп", - "index-roadmap-2027": "2027", - "index-roadmap-2027-title": "Поддержка роста Ваших групп", - "index-roadmap-2027-desc": "Инструменты для продвижения групп", + "index-roadmap-now": "Сейчас", + "index-roadmap-1": "2026", + "index-roadmap-1-title": "Большие каналы и группы", + "index-roadmap-1-desc": "Чтобы Вы могли покинуть централизованные платформы", + "index-roadmap-2": "Июнь 2027", + "index-roadmap-2-title": "Самодостаточные группы и серверы", + "index-roadmap-2-desc": "Запуск Ваучеров Групп", + "index-roadmap-3": "Дек 2027", + "index-roadmap-3-title": "Поддержка роста Ваших групп", + "index-roadmap-3-desc": "Инструменты для продвижения групп", "index-directory-h2": "Вступайте в Группы SimpleX", "index-directory-p1": "Сотни тысяч людей уже доверяют мессенджеру SimpleX.", "index-directory-p2": "Найдите группы по душе в каталоге SimpleX и создайте свои!", diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index e67df60a6d..b5079ac5f4 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -286,12 +286,12 @@ "index-token-p2": "代金券将用于支付服务器费用,让您的社区保持自由和独立。", "index-token-cta": "了解更多信息并获取免费抢先体验券,参与早期测试。", "index-roadmap-h2": "SimpleX 通往自由互联网的路线图", - "index-roadmap-2025-title": "扩展到大型社区", - "index-roadmap-2025-desc": "逃离中心化平台", - "index-roadmap-2026-title": "可持续社区与服务器", - "index-roadmap-2026-desc": "推出社区代金券", - "index-roadmap-2027-title": "促进社区发展", - "index-roadmap-2027-desc": "用于推广社区的工具", + "index-roadmap-1-title": "扩展到大型社区", + "index-roadmap-1-desc": "逃离中心化平台", + "index-roadmap-2-title": "可持续社区与服务器", + "index-roadmap-2-desc": "推出社区代金券", + "index-roadmap-3-title": "促进社区发展", + "index-roadmap-3-desc": "用于推广社区的工具", "index-directory-h2": "加入 SimpleX 社区", "index-directory-p1": "已有数十万人信赖 SimpleX 即时通讯服务。", "index-directory-p2": "在 SimpleX 目录中找到您的社区并创建您自己的社区!", @@ -332,7 +332,7 @@ "file-drop-text": "将文件拖放到此处", "file-drop-hint": "或", "file-choose": "选择文件", - "file-max-size": "最大支持 100 MB - SimpleX Chat 应用 支持最大 1 GB 的文件", + "file-max-size": "最大 100 MB - SimpleX Chat 应用 支持最大 1 GB 文件", "file-encrypting": "加密中……", "file-uploading": "正在上传…", "file-cancel": "取消", @@ -343,7 +343,7 @@ "file-expiry": "文件通常可保存 48 小时。", "file-sec-1": "您的文件已在浏览器中加密 - 数据路由器永远不会看到文件内容、名称或大小。", "file-sec-2": "加密密钥位于链接的哈希片段中——它永远不会发送到任何服务器。", - "file-sec-3": "为了获得更好的安全性,请使用SimpleX Chat应用程序。", + "file-sec-3": "为了获得更好的安全性,请使用SimpleX Chat应用程序。", "file-retry": "重试", "file-downloading": "正在下载…", "file-decrypting": "正在解密…", @@ -365,8 +365,9 @@ "file-proto-p-4": "文件被分割成多个片段后,会通过独立运营商运营的网络路由器进行传输。任何运营商都无法看到文件的实际大小或名称。即使路由器遭到入侵,也只能看到固定大小的加密片段。网络路由器会将文件片段缓存约48小时。", "file-proto-spec": "阅读 XFTP 协议规范 →", "navbar-token": "Token 令牌", - "index-roadmap-2025": "2025", - "index-roadmap-2026": "2026", - "index-roadmap-2027": "2027", + "index-roadmap-now": "现在", + "index-roadmap-1": "2026", + "index-roadmap-2": "2027年6月", + "index-roadmap-3": "2027年12月", "send-file": "发送文件" } diff --git a/website/src/css/design3.css b/website/src/css/design3.css index d2d9b65aac..ea6f20273b 100644 --- a/website/src/css/design3.css +++ b/website/src/css/design3.css @@ -216,6 +216,9 @@ html { font-family: GT-Walsheim, Gilroy, Helvetica, sans-serif; } +body { + overflow-x: hidden; +} .dark html, .dark body { @@ -671,6 +674,40 @@ main .section-bg { background-repeat: no-repeat; } +/* --- Roadmap year labels (CSS text over clean illustration) --- */ +.page-5 .area { + position: relative; +} + +.roadmap-labels { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 2; +} + +.roadmap-label { + position: absolute; + font-family: 'Manrope', sans-serif; + font-weight: 300; + white-space: nowrap; + font-size: calc(var(--sec-vwu) * 1.5); + color: #333333; + /* Isometric projection: rotate then skew */ + transform: rotate(-30deg) skewX(30.6deg) translateX(-50%); + transform-origin: 0 0; +} + +.dark .roadmap-label { + color: #ffffff; +} + +.roadmap-label-now { left: 47.84%; top: 86.87%; } +.roadmap-label-1 { left: 60.8%; top: 73.7%; } +.roadmap-label-2 { left: 73.8%; top: 60.5%; } +.roadmap-label-3 { left: 86.8%; top: 47.2%; } + + .dark .page-5 { background-image: url("/img/design_3/section-5-desktop.webp"); } @@ -751,6 +788,29 @@ main .section-bg { .dark .page-5 { background-image: url("/img/design_3/section-5-mobile.webp"); } + + /* --- Mobile roadmap labels --- */ + .roadmap-label { + transform: none; + transform-origin: center center; + font-size: calc(var(--sec-vwu) * 3.97); + font-weight: 300; + color: #009df7; + background: #fff; + padding: calc(var(--sec-vwu) * 0.2) calc(var(--sec-vwu) * 1.0); + border-radius: calc(var(--sec-vwu) * 0.8); + } + + .dark .roadmap-label { + color: #fff; + } + + .dark .roadmap-label { background: rgba(30, 58, 112, 0.85); } + + .roadmap-label-now { display: none; } + .roadmap-label-1 { left: 75.36%; top: 45.8%; } + .roadmap-label-2 { left: 7.6%; top: 34.9%; } + .roadmap-label-3 { left: 75.36%; top: 24.54%; } } .page .text-container { @@ -770,7 +830,7 @@ main .section-bg { .page .text-container h2 { font-family: "GT-Walsheim", sans-serif; - font-weight: 300; + font-weight: 400; font-size: calc(var(--sec-vwu)*4.94); letter-spacing: -0.025em; line-height: 1.05; diff --git a/website/src/directory.html b/website/src/directory.html index b20e279d82..4ea42f0c3b 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -268,7 +268,7 @@ active_directory: true

Welcome to the selected users' communities that you can join via SimpleX Chat app.

SimpleX Directory is also available as a SimpleX chat bot.

-

Read about how to add your community.

+

Read about how to add your community.