mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
322 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3e5ec8030 | |||
| 4d82afe602 | |||
| 591f74a8e3 | |||
| aede65db14 | |||
| 0bf82c08a1 | |||
| ddef5a122c | |||
| 0fdd2e04cc | |||
| d1e66386f5 | |||
| 708368c9c5 | |||
| 5d8e11c3a2 | |||
| df8ce2b06d | |||
| 6ff5f31bee | |||
| 5e29cda27b | |||
| 2e573662d5 | |||
| 4075c26dd2 | |||
| d64351b760 | |||
| b06211bd4e | |||
| df21d11d33 | |||
| 33bc539e16 | |||
| febea096db | |||
| f0781adbd3 | |||
| df30bb99cb | |||
| 7c86484978 | |||
| 307211a47f | |||
| 7d6c7c58d7 | |||
| 93319d947d | |||
| ea4927c9b0 | |||
| fe0d811bf7 | |||
| cbb3da8f83 | |||
| 83f0bd9fd3 | |||
| 615c483912 | |||
| e0c2272fcb | |||
| 362581432c | |||
| df1a471c56 | |||
| 7d43a43e82 | |||
| ae8ad5c639 | |||
| 1408d75eb3 | |||
| f408988035 | |||
| 945c5015d8 | |||
| 2e431c5afa | |||
| 924273191e | |||
| 9b82cc3303 | |||
| 19e2cebd68 | |||
| 5ef14ca95e | |||
| 886dc56de8 | |||
| bd2ca74987 | |||
| 586671c307 | |||
| ff504702de | |||
| 69e23ad58f | |||
| e9bf229a9d | |||
| 60e0e454e8 | |||
| de76e271a8 | |||
| 5f66c29dbd | |||
| 97cd2682d7 | |||
| 4f8a70a6c1 | |||
| c1a0943448 | |||
| c1c17d1f19 | |||
| 009d13210f | |||
| 892f6498be | |||
| 3fa1d7b07c | |||
| ee146cdc7b | |||
| 219381f941 | |||
| 89f380400e | |||
| 4f1cf6e79f | |||
| 6ff7d4a73c | |||
| 3acc69c6d8 | |||
| b9777c92a5 | |||
| f3be723cde | |||
| a182cf5730 | |||
| 247d12fa40 | |||
| 6593de89c2 | |||
| a1e25620f7 | |||
| 29b9abf241 | |||
| a0b8cf62be | |||
| 43fa4c43a2 | |||
| 85e7a13dba | |||
| 9d992735f4 | |||
| e61babdc8f | |||
| c04e952620 | |||
| 92967dfe0c | |||
| bc96000131 | |||
| a588e7003d | |||
| 665501026d | |||
| a62ce9168e | |||
| f6b611aa30 | |||
| 5a59fdd91c | |||
| 5f01dc1a3f | |||
| c488c4fcd5 | |||
| 3143cc960e | |||
| b8442d92a4 | |||
| 98a3437f43 | |||
| 8f32c6a61a | |||
| 79d5573169 | |||
| a9e7635e00 | |||
| 2c0de36439 | |||
| 94377d0b7a | |||
| e9853fe3fc | |||
| 4738286f4e | |||
| 961bdbfc59 | |||
| 879c117269 | |||
| 03bc4e5d01 | |||
| 80bd4cd337 | |||
| b0f3f0a523 | |||
| 9915e5572e | |||
| 9c6e0a7051 | |||
| 68be4b4ba5 | |||
| 13efdf2595 | |||
| c3991aad87 | |||
| 614846465f | |||
| 096dec2c7b | |||
| d19708ed77 | |||
| 22d7db89d8 | |||
| ba7abcf6f7 | |||
| b512d3a743 | |||
| 9fa968a593 | |||
| 41b7ad01f9 | |||
| 15fae29e5b | |||
| 8c1abcccfb | |||
| 25893177d0 | |||
| 345e0acdec | |||
| 1f04984a34 | |||
| 7a91ed2ab2 | |||
| d912fe07a1 | |||
| cfc21dfb51 | |||
| e5c83b20c9 | |||
| 97b472fd9c | |||
| d40d690f86 | |||
| 1730c5136f | |||
| 06bb29e87d | |||
| 05cc300231 | |||
| cd65e4dbb9 | |||
| f5d5bd62ed | |||
| d2f9888195 | |||
| 5ec5826642 | |||
| 29155ab73a | |||
| 31782600cb | |||
| db392037ea | |||
| 6ad7189c53 | |||
| 15d8444c74 | |||
| 2d31b853ac | |||
| 3af0108b0d | |||
| 746dd33728 | |||
| 2fceecab37 | |||
| e094ec5cf5 | |||
| fbd6012611 | |||
| 913ce4ebfd | |||
| 1e01a961fe | |||
| 27f4a71146 | |||
| 282bca4d81 | |||
| e5011ad150 | |||
| 0d4d08e52b | |||
| 7f8c7b2d99 | |||
| f5b666ae6d | |||
| 1cfe51af45 | |||
| 68af58bda1 | |||
| f51796b81c | |||
| 995618cf1c | |||
| 68baa18af8 | |||
| a4f1c9c915 | |||
| b8141c3555 | |||
| 47214e33ea | |||
| ff239d81e5 | |||
| 0a6e47dd61 | |||
| ee75219ed3 | |||
| 2e337a753f | |||
| 9918d91def | |||
| 82dd5751c1 | |||
| de637fab50 | |||
| cb49f6f01d | |||
| 6d0a83aa58 | |||
| 05f57e98ad | |||
| 6a66525927 | |||
| 9530a6055a | |||
| f5ed8debcc | |||
| 355d2449c5 | |||
| f7382cdd6f | |||
| dc8d10d068 | |||
| 951245d33f | |||
| 521b901cc9 | |||
| 6311ba451b | |||
| 754c76d6fd | |||
| 545f110fb8 | |||
| 4c6387c854 | |||
| aea7ff1c89 | |||
| 77ac972e09 | |||
| 256f85024f | |||
| 870f9e42dd | |||
| b3544cfbc3 | |||
| 0a4a3a24e1 | |||
| 8a66390a78 | |||
| 3d48eded3d | |||
| 6546426ec0 | |||
| 4a7ceb00fb | |||
| 53560378bb | |||
| bfa8717ed4 | |||
| cdb3b6aafd | |||
| 9f3d3e8ba4 | |||
| 047aad592e | |||
| 087acd9180 | |||
| 0b822e4a5c | |||
| f8a469488e | |||
| 3b5e806418 | |||
| 79e208193a | |||
| ef5c13b1c1 | |||
| 38533213d2 | |||
| 5f1aa6fa9d | |||
| b0002fe07d | |||
| ef21fd1d26 | |||
| 4a311b9578 | |||
| b8da5e225b | |||
| 8b3300e197 | |||
| f27de052cf | |||
| 5cc537f14c | |||
| dc8ca4cf89 | |||
| b62dd801f1 | |||
| 0c096e2c89 | |||
| cc127e56fe | |||
| 1781495ee3 | |||
| 831231d8e6 | |||
| 45102442f4 | |||
| f323c8e112 | |||
| 3bdc6b5e28 | |||
| d8373262bc | |||
| 3597d34716 | |||
| bd4259e89e | |||
| 55ead740cc | |||
| 5ef0eda2d7 | |||
| 49a9b0e7d6 | |||
| 45ada450a2 | |||
| 307a1b3c5e | |||
| ed6b3bbead | |||
| 901610eec5 | |||
| 7d4127c51d | |||
| 13215d91d7 | |||
| e1a8099474 | |||
| daa8d9bb21 | |||
| 5fcbade1bc | |||
| 3937ffa9a6 | |||
| 80ddb50e1c | |||
| f6e66f1c53 | |||
| 0c23ff9ae3 | |||
| 1570bc2b99 | |||
| 1e2104cabf | |||
| f3014f258d | |||
| f0991cc0ba | |||
| 74b78a8d7b | |||
| 82cd70a75c | |||
| fe4eb7b5af | |||
| c459e71d02 | |||
| 2516d5a393 | |||
| 477d98d75a | |||
| 4253cd7fb9 | |||
| ca78958667 | |||
| 1f5b80d560 | |||
| 2de111e76c | |||
| 8343285d93 | |||
| 5dbe2b2745 | |||
| fb9485190d | |||
| 6881600e06 | |||
| 9ed723bafa | |||
| 9ded1c9821 | |||
| bb374c68b1 | |||
| c3e82a6a4e | |||
| 7c12e82042 | |||
| e7e66ff873 | |||
| c4d7e5307c | |||
| d6b9a45a39 | |||
| 7fd3b4d6ba | |||
| 4004aafbc5 | |||
| 95008eeeaf | |||
| c7a8992043 | |||
| ea2b5f2ccf | |||
| ed9f277421 | |||
| 5c14c3b349 | |||
| d8fb31f167 | |||
| 02db38ffd3 | |||
| 7692195bfa | |||
| c435cbdc7b | |||
| effc281271 | |||
| 41eb2e5689 | |||
| 67d74a0a27 | |||
| c60af078d7 | |||
| f66405e79b | |||
| 74d186af16 | |||
| e4fbe66d95 | |||
| 187fef0c5a | |||
| 4782cab507 | |||
| cb52d75ff0 | |||
| bcbee67709 | |||
| 2501cbe55d | |||
| 2bd049db87 | |||
| 6b8b9ab4fd | |||
| 30db24265e | |||
| 316d605899 | |||
| b4257f7767 | |||
| a3f2d5c919 | |||
| cf46469cd5 | |||
| 0312fde818 | |||
| 9defa44f0c | |||
| 915b53054c | |||
| f81557b4fd | |||
| e273bd1239 | |||
| a63caf4640 | |||
| e7f0234134 | |||
| 340552321e | |||
| 98a3fc214d | |||
| 6a578cfe3c | |||
| dacc075fe8 | |||
| 55418e2bc0 | |||
| f2b5c0f3a8 | |||
| 5ebdf5dba9 | |||
| 8e045764df | |||
| 503d3d77e6 | |||
| 81bd7d97c5 | |||
| 8f57925067 | |||
| 9bf99db82e | |||
| 5615cdbf1a | |||
| d802ae0058 | |||
| 8f2278198c | |||
| 10937a5a4e | |||
| 6aff6e9804 | |||
| 95477cae7e |
@@ -102,7 +102,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Haskell
|
||||
uses: haskell-actions/setup@v2
|
||||
uses: haskell/actions/setup@v2
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: "3.10.1.0"
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
|
||||
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
|
||||
run: |
|
||||
scripts/ci/build-desktop-mac.sh
|
||||
scripts/build-desktop-mac.sh
|
||||
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
|
||||
echo "package_path=$path" >> $GITHUB_OUTPUT
|
||||
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||
@@ -310,7 +310,9 @@ jobs:
|
||||
# Unix /
|
||||
|
||||
# / Windows
|
||||
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
|
||||
|
||||
# * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753
|
||||
# * And GitHub Actions does not support parameterizing shell in a matrix job - https://github.community/t/using-matrix-to-specify-shell-is-it-possible/17065
|
||||
|
||||
- name: 'Setup MSYS2'
|
||||
if: matrix.os == 'windows-latest'
|
||||
|
||||
@@ -233,6 +233,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent and important updates:
|
||||
|
||||
[Dec 10, 2024. SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md)
|
||||
|
||||
[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md)
|
||||
|
||||
[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md)
|
||||
@@ -241,20 +243,14 @@ Recent and important updates:
|
||||
|
||||
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
||||
|
||||
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
||||
|
||||
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
||||
|
||||
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
|
||||
|
||||
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
||||
|
||||
[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
|
||||
[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
[All updates](./blog)
|
||||
|
||||
## :zap: Quick installation of a terminal app
|
||||
@@ -382,9 +378,11 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
||||
- ✅ Improve sending videos (including encryption of locally stored videos).
|
||||
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
|
||||
- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- ✅ Support multiple network operators in the app.
|
||||
- 🏗 Large groups, communities and public channels.
|
||||
- 🏗 Short links to connect and join groups.
|
||||
- 🏗 Improve stability and reduce battery usage.
|
||||
- 🏗 Improve experience for the new users.
|
||||
- 🏗 Large groups, communities and public channels.
|
||||
- Privacy & security slider - a simple way to set all settings at once.
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- Include optional message into connection request sent via contact address.
|
||||
|
||||
@@ -17,6 +17,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
application.registerForRemoteNotifications()
|
||||
removePasscodesIfReinstalled()
|
||||
prepareForLaunch()
|
||||
deleteOldChatArchive()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -845,7 +845,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func dismissConnReqView(_ id: String) {
|
||||
if id == showingInvitation?.connId {
|
||||
if id == showingInvitation?.pcc.id {
|
||||
markShowingInvitationUsed()
|
||||
dismissAllSheets()
|
||||
}
|
||||
@@ -898,7 +898,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
struct ShowingInvitation {
|
||||
var connId: String
|
||||
var pcc: PendingContactConnection
|
||||
var connChatUsed: Bool
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ enum NtfCallAction {
|
||||
class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
static let shared = NtfManager()
|
||||
|
||||
public var navigatingToChat = false
|
||||
private var granted = false
|
||||
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
|
||||
|
||||
@@ -74,7 +75,10 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
}
|
||||
} else {
|
||||
if let chatId = content.targetContentIdentifier {
|
||||
ItemsModel.shared.loadOpenChat(chatId)
|
||||
self.navigatingToChat = true
|
||||
ItemsModel.shared.loadOpenChat(chatId) {
|
||||
self.navigatingToChat = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,13 @@ func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, re
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction) async throws -> [MemberReaction] {
|
||||
let userId = try currentUserId("apiGetReactionMemebers")
|
||||
let r = await chatSendCmd(.apiGetReactionMembers(userId: userId, groupId: groupId, itemId: itemId, reaction: reaction ))
|
||||
if case let .reactionMembers(_, memberReactions) = r { return memberReactions }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
||||
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemsDeleted(_, items, _) = r { return items }
|
||||
@@ -512,7 +519,14 @@ func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFail
|
||||
throw r
|
||||
}
|
||||
|
||||
func getServerOperators() throws -> ServerOperatorConditions {
|
||||
func getServerOperators() async throws -> ServerOperatorConditions {
|
||||
let r = await chatSendCmd(.apiGetServerOperators)
|
||||
if case let .serverOperatorConditions(conditions) = r { return conditions }
|
||||
logger.error("getServerOperators error: \(String(describing: r))")
|
||||
throw r
|
||||
}
|
||||
|
||||
func getServerOperatorsSync() throws -> ServerOperatorConditions {
|
||||
let r = chatSendCmdSync(.apiGetServerOperators)
|
||||
if case let .serverOperatorConditions(conditions) = r { return conditions }
|
||||
logger.error("getServerOperators error: \(String(describing: r))")
|
||||
@@ -1061,8 +1075,8 @@ func apiRejectContactRequest(contactReqId: Int64) async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async throws {
|
||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
|
||||
func apiChatRead(type: ChatType, id: Int64) async throws {
|
||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id))
|
||||
}
|
||||
|
||||
func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
|
||||
@@ -1368,15 +1382,13 @@ func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
|
||||
throw r
|
||||
}
|
||||
|
||||
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
|
||||
func markChatRead(_ chat: Chat) async {
|
||||
do {
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
let minItemId = chat.chatStats.minUnreadItemId
|
||||
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId)
|
||||
await MainActor.run {
|
||||
withAnimation { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
|
||||
withAnimation { ChatModel.shared.markChatItemsRead(cInfo) }
|
||||
}
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
@@ -1399,17 +1411,6 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
|
||||
}
|
||||
}
|
||||
|
||||
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
|
||||
do {
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.markChatItemsRead(cInfo, [cItem.id])
|
||||
}
|
||||
} catch {
|
||||
logger.error("apiChatRead error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async {
|
||||
do {
|
||||
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
|
||||
@@ -1605,7 +1606,16 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
m.chatInitialized = true
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
m.conditions = try getServerOperators()
|
||||
m.conditions = try getServerOperatorsSync()
|
||||
if shouldImportAppSettingsDefault.get() {
|
||||
do {
|
||||
let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
appSettings.importIntoApp()
|
||||
shouldImportAppSettingsDefault.set(false)
|
||||
} catch {
|
||||
logger.error("Error while importing app settings: \(error)")
|
||||
}
|
||||
}
|
||||
if m.currentUser == nil {
|
||||
onboardingStageDefault.set(.step1_SimpleXInfo)
|
||||
privacyDeliveryReceiptsSet.set(true)
|
||||
@@ -2011,6 +2021,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.removeChat(hostConn.id)
|
||||
}
|
||||
}
|
||||
case let .businessLinkConnecting(user, groupInfo, _, fromContact):
|
||||
if !active(user) { return }
|
||||
|
||||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
if m.chatId == fromContact.id {
|
||||
ItemsModel.shared.loadOpenChat(groupInfo.id)
|
||||
}
|
||||
await MainActor.run {
|
||||
m.removeChat(fromContact.id)
|
||||
}
|
||||
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
|
||||
@@ -143,7 +143,8 @@ struct SimpleXApp: App {
|
||||
let chats = try await apiGetChatsAsync()
|
||||
await MainActor.run { chatModel.updateChats(chats) }
|
||||
if let id = chatModel.chatId,
|
||||
let chat = chatModel.getChat(id) {
|
||||
let chat = chatModel.getChat(id),
|
||||
!NtfManager.shared.navigatingToChat {
|
||||
Task { await loadChat(chat: chat, clearItems: false) }
|
||||
}
|
||||
if let ncr = chatModel.ntfContactRequest {
|
||||
|
||||
@@ -45,7 +45,7 @@ struct ChatInfoToolbar: View {
|
||||
}
|
||||
|
||||
private var contactVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.baselineOffset(1)
|
||||
|
||||
@@ -96,6 +96,8 @@ struct ChatInfoView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@State var contact: Contact
|
||||
@State var localAlias: String
|
||||
@State var featuresAllowed: ContactFeaturesAllowed
|
||||
@State var currentFeaturesAllowed: ContactFeaturesAllowed
|
||||
var onSearch: () -> Void
|
||||
@State private var connectionStats: ConnectionStats? = nil
|
||||
@State private var customUserProfile: Profile? = nil
|
||||
@@ -154,8 +156,8 @@ struct ChatInfoView: View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
let buttonWidth = g.size.width / 4
|
||||
searchButton(width: buttonWidth)
|
||||
AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
AudioCallButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
VideoButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
muteButton(width: buttonWidth)
|
||||
}
|
||||
}
|
||||
@@ -312,7 +314,15 @@ struct ChatInfoView: View {
|
||||
case .networkStatusAlert: return networkStatusAlert()
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
||||
case .syncConnectionForceAlert:
|
||||
return syncConnectionForceAlert({
|
||||
Task {
|
||||
if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) {
|
||||
connectionStats = stats
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
case let .queueInfo(info): return queueInfoAlert(info)
|
||||
case let .someAlert(a): return a.alert
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
@@ -327,6 +337,16 @@ struct ChatInfoView: View {
|
||||
$0.content
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if currentFeaturesAllowed != featuresAllowed {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Save preferences?", comment: "alert title"),
|
||||
buttonTitle: NSLocalizedString("Save and notify contact", comment: "alert button"),
|
||||
buttonAction: { savePreferences() },
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func contactInfoHeader() -> some View {
|
||||
@@ -339,7 +359,7 @@ struct ChatInfoView: View {
|
||||
Text(Image(systemName: "checkmark.shield"))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.title2)
|
||||
+ Text(" ")
|
||||
+ textSpace
|
||||
+ Text(contact.profile.displayName)
|
||||
.font(.largeTitle)
|
||||
)
|
||||
@@ -447,8 +467,9 @@ struct ChatInfoView: View {
|
||||
NavigationLink {
|
||||
ContactPreferencesView(
|
||||
contact: $contact,
|
||||
featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
|
||||
featuresAllowed: $featuresAllowed,
|
||||
currentFeaturesAllowed: $currentFeaturesAllowed,
|
||||
savePreferences: savePreferences
|
||||
)
|
||||
.navigationBarTitle("Contact preferences")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
@@ -480,7 +501,12 @@ struct ChatInfoView: View {
|
||||
|
||||
private func synchronizeConnectionButton() -> some View {
|
||||
Button {
|
||||
syncContactConnection(force: false)
|
||||
Task {
|
||||
if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) {
|
||||
connectionStats = stats
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
|
||||
.foregroundColor(.orange)
|
||||
@@ -599,29 +625,50 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func syncContactConnection(force: Bool) {
|
||||
private func savePreferences() {
|
||||
Task {
|
||||
do {
|
||||
let stats = try apiSyncContactRatchet(contact.apiId, force)
|
||||
connectionStats = stats
|
||||
await MainActor.run {
|
||||
chatModel.updateContactConnectionStats(contact, stats)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error synchronizing connection")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
let prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
|
||||
if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) {
|
||||
await MainActor.run {
|
||||
contact = toContact
|
||||
chatModel.updateContact(toContact)
|
||||
currentFeaturesAllowed = featuresAllowed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? {
|
||||
do {
|
||||
let stats = try apiSyncContactRatchet(contact.apiId, force)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateContactConnectionStats(contact, stats)
|
||||
}
|
||||
return stats
|
||||
} catch let error {
|
||||
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error synchronizing connection")
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
SomeAlert(
|
||||
alert: mkAlert(title: a.title, message: a.message),
|
||||
id: "syncContactConnection error"
|
||||
)
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioCallButton: View {
|
||||
var chat: Chat
|
||||
var contact: Contact
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
var width: CGFloat
|
||||
var showAlert: (SomeAlert) -> Void
|
||||
|
||||
@@ -629,6 +676,7 @@ struct AudioCallButton: View {
|
||||
CallButton(
|
||||
chat: chat,
|
||||
contact: contact,
|
||||
connectionStats: $connectionStats,
|
||||
image: "phone.fill",
|
||||
title: "call",
|
||||
mediaType: .audio,
|
||||
@@ -641,6 +689,7 @@ struct AudioCallButton: View {
|
||||
struct VideoButton: View {
|
||||
var chat: Chat
|
||||
var contact: Contact
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
var width: CGFloat
|
||||
var showAlert: (SomeAlert) -> Void
|
||||
|
||||
@@ -648,6 +697,7 @@ struct VideoButton: View {
|
||||
CallButton(
|
||||
chat: chat,
|
||||
contact: contact,
|
||||
connectionStats: $connectionStats,
|
||||
image: "video.fill",
|
||||
title: "video",
|
||||
mediaType: .video,
|
||||
@@ -660,6 +710,7 @@ struct VideoButton: View {
|
||||
private struct CallButton: View {
|
||||
var chat: Chat
|
||||
var contact: Contact
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
var image: String
|
||||
var title: LocalizedStringKey
|
||||
var mediaType: CallMediaType
|
||||
@@ -671,12 +722,40 @@ private struct CallButton: View {
|
||||
|
||||
InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) {
|
||||
if canCall {
|
||||
if CallController.useCallKit() {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
} else {
|
||||
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
|
||||
dismissAllSheets(animated: true) {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
if let connStats = connectionStats {
|
||||
if connStats.ratchetSyncState == .ok {
|
||||
if CallController.useCallKit() {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
} else {
|
||||
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
|
||||
dismissAllSheets(animated: true) {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
}
|
||||
}
|
||||
} else if connStats.ratchetSyncAllowed {
|
||||
showAlert(SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Fix connection?"),
|
||||
message: Text("Connection requires encryption renegotiation."),
|
||||
primaryButton: .default(Text("Fix")) {
|
||||
Task {
|
||||
if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) {
|
||||
connectionStats = stats
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
),
|
||||
id: "can't call contact, fix connection"
|
||||
))
|
||||
} else {
|
||||
showAlert(SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: "Can't call contact",
|
||||
message: "Encryption renegotiation in progress."
|
||||
),
|
||||
id: "can't call contact, encryption renegotiation in progress"
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if contact.nextSendGrpInv {
|
||||
@@ -1173,6 +1252,8 @@ struct ChatInfoView_Previews: PreviewProvider {
|
||||
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
|
||||
contact: Contact.sampleData,
|
||||
localAlias: "",
|
||||
featuresAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences),
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences),
|
||||
onSearch: {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ struct CIFeaturePreferenceView: View {
|
||||
+ Text(acceptText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
+ Text(" ")
|
||||
+ Text(verbatim: " ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
|
||||
@@ -45,7 +45,7 @@ struct CIGroupInvitationView: View {
|
||||
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
||||
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ Text(verbatim: " ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
@@ -53,7 +53,7 @@ struct CIGroupInvitationView: View {
|
||||
} else {
|
||||
(
|
||||
groupInvitationText()
|
||||
+ Text(" ")
|
||||
+ Text(verbatim: " ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
|
||||
@@ -23,7 +23,7 @@ struct CIMemberCreatedContactView: View {
|
||||
.onTapGesture {
|
||||
dismissAllSheets(animated: true)
|
||||
DispatchQueue.main.async {
|
||||
m.chatId = "@\(contactId)"
|
||||
ItemsModel.shared.loadOpenChat("@\(contactId)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -45,7 +45,7 @@ struct CIMemberCreatedContactView: View {
|
||||
+ Text(openText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
+ Text(" ")
|
||||
+ Text(verbatim: " ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
|
||||
@@ -83,7 +83,7 @@ enum MetaColorMode {
|
||||
? Image("checkmark.wide")
|
||||
: Image(systemName: "circlebadge.fill")
|
||||
).foregroundColor(.clear)
|
||||
case .invertedMaterial: Text(" ").kerning(13)
|
||||
case .invertedMaterial: textSpace.kerning(13)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func ciMetaText(
|
||||
if ttl != chatTTL {
|
||||
r = r + colored(Text(shortTimeText(ttl)), resolved)
|
||||
}
|
||||
space = Text(" ")
|
||||
space = textSpace
|
||||
}
|
||||
if showViaProxy, meta.sentViaProxy == true {
|
||||
appendSpace()
|
||||
@@ -138,12 +138,12 @@ func ciMetaText(
|
||||
} else if !meta.disappearing {
|
||||
r = r + colorMode.statusSpacer(meta.itemStatus.sent)
|
||||
}
|
||||
space = Text(" ")
|
||||
space = textSpace
|
||||
}
|
||||
if let enc = encrypted {
|
||||
appendSpace()
|
||||
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
|
||||
space = Text(" ")
|
||||
space = textSpace
|
||||
}
|
||||
if showTimesamp {
|
||||
appendSpace()
|
||||
|
||||
@@ -121,11 +121,11 @@ struct CIRcvDecryptionError: View {
|
||||
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ textSpace
|
||||
+ Text("Fix connection")
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ Text(verbatim: " ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
}
|
||||
@@ -144,7 +144,7 @@ struct CIRcvDecryptionError: View {
|
||||
Text(chatItem.content.text)
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ Text(verbatim: " ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
@@ -17,7 +17,7 @@ struct MarkedDeletedItemView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
|
||||
(Text(mergedMarkedDeletedText).italic() + textSpace + chatItem.timestampText)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||
|
||||
private let noTyping = Text(" ")
|
||||
private let noTyping = Text(verbatim: " ")
|
||||
|
||||
private let typingIndicators: [Text] = [
|
||||
(typing(.black) + typing() + typing()),
|
||||
@@ -85,7 +85,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
(rightToLeft ? Text("\n") : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
|
||||
}
|
||||
|
||||
if let i = icon {
|
||||
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + Text(" ") + res
|
||||
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + textSpace + res
|
||||
}
|
||||
|
||||
if let s = sender {
|
||||
|
||||
@@ -170,7 +170,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
|
||||
private func eventItemViewText(_ secondaryColor: Color) -> Text {
|
||||
if !revealed, let t = mergedGroupEventText {
|
||||
return chatEventText(t + Text(" ") + chatItem.timestampText, secondaryColor)
|
||||
return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor)
|
||||
} else if let member = chatItem.memberDisplayName {
|
||||
return Text(member + " ")
|
||||
.font(.caption)
|
||||
@@ -203,7 +203,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
} else if ns.count == 0 {
|
||||
Text("\(count) group events")
|
||||
} else if count > ns.count {
|
||||
Text(members) + Text(" ") + Text("and \(count - ns.count) other events")
|
||||
Text(members) + textSpace + Text("and \(count - ns.count) other events")
|
||||
} else {
|
||||
Text(members)
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text {
|
||||
}
|
||||
|
||||
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text {
|
||||
chatEventText(Text(eventText) + Text(" ") + ts, secondaryColor)
|
||||
chatEventText(Text(eventText) + textSpace + ts, secondaryColor)
|
||||
}
|
||||
|
||||
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
|
||||
|
||||
@@ -133,7 +133,12 @@ struct ChatView: View {
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
Group {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true)
|
||||
GroupMemberInfoView(
|
||||
groupInfo: groupInfo,
|
||||
chat: chat,
|
||||
groupMember: member,
|
||||
navigation: true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +231,8 @@ struct ChatView: View {
|
||||
chat: chat,
|
||||
contact: contact,
|
||||
localAlias: chat.chatInfo.localAlias,
|
||||
featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
|
||||
onSearch: { focusSearch() }
|
||||
)
|
||||
}
|
||||
@@ -433,6 +440,7 @@ struct ChatView: View {
|
||||
maxWidth: maxWidth,
|
||||
composeState: $composeState,
|
||||
selectedMember: $selectedMember,
|
||||
showChatInfoSheet: $showChatInfoSheet,
|
||||
revealedChatItem: $revealedChatItem,
|
||||
selectedChatItems: $selectedChatItems,
|
||||
forwardedChatItems: $forwardedChatItems
|
||||
@@ -886,12 +894,14 @@ struct ChatView: View {
|
||||
private struct ChatItemWithMenu: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
|
||||
@Binding @ObservedObject var chat: Chat
|
||||
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
|
||||
let chatItem: ChatItem
|
||||
let maxWidth: CGFloat
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var selectedMember: GMember?
|
||||
@Binding var showChatInfoSheet: Bool
|
||||
@Binding var revealedChatItem: ChatItem?
|
||||
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@@ -901,7 +911,7 @@ struct ChatView: View {
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var msgWidth: CGFloat = 0
|
||||
|
||||
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
@Binding var forwardedChatItems: [ChatItem]
|
||||
|
||||
@@ -987,7 +997,7 @@ struct ChatView: View {
|
||||
}
|
||||
} else if chatItem.isRcvNew {
|
||||
waitToMarkRead {
|
||||
await apiMarkChatItemRead(chat.chatInfo, chatItem)
|
||||
await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1117,14 +1127,13 @@ struct ChatView: View {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||
.onTapGesture {
|
||||
if let member = m.getGroupMember(member.groupMemberId) {
|
||||
selectedMember = member
|
||||
if let mem = m.getGroupMember(member.groupMemberId) {
|
||||
selectedMember = mem
|
||||
} else {
|
||||
Task {
|
||||
await m.loadGroupMembers(groupInfo) {
|
||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
||||
}
|
||||
}
|
||||
let mem = GMember.init(member)
|
||||
m.groupMembers.append(mem)
|
||||
m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1
|
||||
selectedMember = mem
|
||||
}
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
@@ -1244,12 +1253,27 @@ struct ChatView: View {
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 4)
|
||||
|
||||
if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) {
|
||||
.if(chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted)) { v in
|
||||
v.onTapGesture {
|
||||
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo):
|
||||
v.contextMenu {
|
||||
ReactionContextMenu(
|
||||
groupInfo: groupInfo,
|
||||
itemId: ci.id,
|
||||
reactionCount: r,
|
||||
selectedMember: $selectedMember,
|
||||
profileRadius: profileRadius
|
||||
)
|
||||
}
|
||||
case let .direct(contact):
|
||||
v.contextMenu {
|
||||
contactReactionMenu(contact, r)
|
||||
}
|
||||
default:
|
||||
v
|
||||
}
|
||||
}
|
||||
@@ -1752,6 +1776,20 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View {
|
||||
if !r.userReacted || r.totalReacted > 1 {
|
||||
Button { showChatInfoSheet = true } label: {
|
||||
profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius)
|
||||
}
|
||||
}
|
||||
if r.userReacted {
|
||||
Button {} label: {
|
||||
profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius)
|
||||
}
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SelectedChatItem: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@@ -1838,6 +1876,104 @@ private func buildTheme() -> AppTheme {
|
||||
}
|
||||
}
|
||||
|
||||
struct ReactionContextMenu: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
let groupInfo: GroupInfo
|
||||
var itemId: Int64
|
||||
var reactionCount: CIReactionCount
|
||||
@Binding var selectedMember: GMember?
|
||||
var profileRadius: CGFloat
|
||||
@State private var memberReactions: [MemberReaction] = []
|
||||
|
||||
var body: some View {
|
||||
groupMemberReactionList()
|
||||
.task {
|
||||
await loadChatItemReaction()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func groupMemberReactionList() -> some View {
|
||||
if memberReactions.isEmpty {
|
||||
ForEach(Array(repeating: 0, count: reactionCount.totalReacted), id: \.self) { _ in
|
||||
Text(verbatim: " ")
|
||||
}
|
||||
} else {
|
||||
ForEach(memberReactions, id: \.groupMember.groupMemberId) { mr in
|
||||
let mem = mr.groupMember
|
||||
let userMember = mem.groupMemberId == groupInfo.membership.groupMemberId
|
||||
Button {
|
||||
if let member = m.getGroupMember(mem.groupMemberId) {
|
||||
selectedMember = member
|
||||
} else {
|
||||
let member = GMember.init(mem)
|
||||
m.groupMembers.append(member)
|
||||
m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1
|
||||
selectedMember = member
|
||||
}
|
||||
} label: {
|
||||
profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius)
|
||||
}
|
||||
.disabled(userMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadChatItemReaction() async {
|
||||
do {
|
||||
let memberReactions = try await apiGetReactionMembers(
|
||||
groupId: groupInfo.groupId,
|
||||
itemId: itemId,
|
||||
reaction: reactionCount.reaction
|
||||
)
|
||||
await MainActor.run {
|
||||
self.memberReactions = memberReactions
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiGetReactionMembers error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View {
|
||||
HStack {
|
||||
nameText
|
||||
if let image, let img = imageFromBase64(image) {
|
||||
Image(uiImage: maskToCustomShape(img, size: 30, radius: radius))
|
||||
} else {
|
||||
Image(systemName: "person.crop.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage {
|
||||
let path = Path { path in
|
||||
if radius >= 50 {
|
||||
path.addEllipse(in: CGRect(x: 0, y: 0, width: size, height: size))
|
||||
} else if radius <= 0 {
|
||||
path.addRect(CGRect(x: 0, y: 0, width: size, height: size))
|
||||
} else {
|
||||
let cornerRadius = size * CGFloat(radius) / 100
|
||||
path.addRoundedRect(
|
||||
in: CGRect(x: 0, y: 0, width: size, height: size),
|
||||
cornerSize: CGSize(width: cornerRadius, height: cornerRadius),
|
||||
style: .continuous
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { context in
|
||||
context.cgContext.addPath(path.cgPath)
|
||||
context.cgContext.clip()
|
||||
let scale = size / max(image.size.width, image.size.height)
|
||||
let imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
|
||||
let imageOrigin = CGPoint(
|
||||
x: (size - imageSize.width) / 2,
|
||||
y: (size - imageSize.height) / 2
|
||||
)
|
||||
image.draw(in: CGRect(origin: imageOrigin, size: imageSize))
|
||||
}
|
||||
}
|
||||
|
||||
struct ToggleNtfsButton: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ struct ContextItemView: View {
|
||||
}
|
||||
|
||||
func image(_ s: String) -> Text {
|
||||
Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ")
|
||||
Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + textSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ struct ContactPreferencesView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var contact: Contact
|
||||
@State var featuresAllowed: ContactFeaturesAllowed
|
||||
@State var currentFeaturesAllowed: ContactFeaturesAllowed
|
||||
@Binding var featuresAllowed: ContactFeaturesAllowed
|
||||
@Binding var currentFeaturesAllowed: ContactFeaturesAllowed
|
||||
@State private var showSaveDialogue = false
|
||||
let savePreferences: () -> Void
|
||||
|
||||
var body: some View {
|
||||
let user: User = chatModel.currentUser!
|
||||
@@ -48,7 +49,10 @@ struct ContactPreferencesView: View {
|
||||
savePreferences()
|
||||
dismiss()
|
||||
}
|
||||
Button("Exit without saving") { dismiss() }
|
||||
Button("Exit without saving") {
|
||||
featuresAllowed = currentFeaturesAllowed
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,31 +122,15 @@ struct ContactPreferencesView: View {
|
||||
private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View {
|
||||
Text(feature.enabledDescription(enabled))
|
||||
}
|
||||
|
||||
private func savePreferences() {
|
||||
Task {
|
||||
do {
|
||||
let prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
|
||||
if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) {
|
||||
await MainActor.run {
|
||||
contact = toContact
|
||||
chatModel.updateContact(toContact)
|
||||
currentFeaturesAllowed = featuresAllowed
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactPreferencesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContactPreferencesView(
|
||||
contact: Binding.constant(Contact.sampleData),
|
||||
featuresAllowed: ContactFeaturesAllowed.sampleData,
|
||||
currentFeaturesAllowed: ContactFeaturesAllowed.sampleData
|
||||
featuresAllowed: Binding.constant(ContactFeaturesAllowed.sampleData),
|
||||
currentFeaturesAllowed: Binding.constant(ContactFeaturesAllowed.sampleData),
|
||||
savePreferences: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,12 @@ struct AddGroupMembersViewCommon: View {
|
||||
let count = selectedContacts.count
|
||||
Section {
|
||||
if creatingGroup {
|
||||
groupPreferencesButton($groupInfo, true)
|
||||
GroupPreferencesButton(
|
||||
groupInfo: $groupInfo,
|
||||
preferences: groupInfo.fullGroupPreferences,
|
||||
currentPreferences: groupInfo.fullGroupPreferences,
|
||||
creatingGroup: true
|
||||
)
|
||||
}
|
||||
rolePicker()
|
||||
inviteMembersButton()
|
||||
@@ -140,12 +145,13 @@ struct AddGroupMembersViewCommon: View {
|
||||
return dummy
|
||||
}()
|
||||
|
||||
private func inviteMembersButton() -> some View {
|
||||
@ViewBuilder private func inviteMembersButton() -> some View {
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat"
|
||||
Button {
|
||||
inviteMembers()
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Invite to group")
|
||||
Text(label)
|
||||
Image(systemName: "checkmark")
|
||||
}
|
||||
}
|
||||
@@ -231,6 +237,7 @@ func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding,
|
||||
.focused(focussed)
|
||||
.foregroundColor(onBackgroundColor)
|
||||
.frame(maxWidth: .infinity)
|
||||
.autocorrectionDisabled(true)
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
|
||||
@@ -81,13 +81,13 @@ struct GroupChatInfoView: View {
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
|
||||
Section {
|
||||
if groupInfo.canEdit {
|
||||
if groupInfo.isOwner && groupInfo.businessChat == nil {
|
||||
editGroupButton()
|
||||
}
|
||||
if groupInfo.groupProfile.description != nil || groupInfo.canEdit {
|
||||
if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) {
|
||||
addOrEditWelcomeMessage()
|
||||
}
|
||||
groupPreferencesButton($groupInfo)
|
||||
GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences)
|
||||
if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
|
||||
sendReceiptsOption()
|
||||
} else {
|
||||
@@ -101,13 +101,20 @@ struct GroupChatInfoView: View {
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
Text("Only group owners can change group preferences.")
|
||||
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)
|
||||
}
|
||||
|
||||
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
|
||||
if groupInfo.canAddMembers {
|
||||
groupLinkButton()
|
||||
if groupInfo.businessChat == nil {
|
||||
groupLinkButton()
|
||||
}
|
||||
if (chat.chatInfo.incognito) {
|
||||
Label("Invite members", systemImage: "plus")
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
@@ -276,10 +283,15 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
private func addMembersButton() -> some View {
|
||||
NavigationLink {
|
||||
let label: LocalizedStringKey = switch groupInfo.businessChat?.chatType {
|
||||
case .customer: "Add team members"
|
||||
case .business: "Add friends"
|
||||
case .none: "Invite members"
|
||||
}
|
||||
return NavigationLink {
|
||||
addMembersDestinationView()
|
||||
} label: {
|
||||
Label("Invite members", systemImage: "plus")
|
||||
Label(label, systemImage: "plus")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +430,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
private var memberVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||
.font(.caption)
|
||||
.baselineOffset(2)
|
||||
.kerning(-2)
|
||||
@@ -427,7 +439,7 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
private func memberInfoView(_ groupMember: GMember) -> some View {
|
||||
GroupMemberInfoView(groupInfo: groupInfo, groupMember: groupMember)
|
||||
GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember)
|
||||
.navigationBarHidden(false)
|
||||
}
|
||||
|
||||
@@ -487,11 +499,12 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteGroupButton() -> some View {
|
||||
@ViewBuilder private func deleteGroupButton() -> some View {
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group" : "Delete chat"
|
||||
Button(role: .destructive) {
|
||||
alert = .deleteGroupAlert
|
||||
} label: {
|
||||
Label("Delete group", systemImage: "trash")
|
||||
Label(label, systemImage: "trash")
|
||||
.foregroundColor(Color.red)
|
||||
}
|
||||
}
|
||||
@@ -505,20 +518,22 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func leaveGroupButton() -> some View {
|
||||
@ViewBuilder private func leaveGroupButton() -> some View {
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat"
|
||||
Button(role: .destructive) {
|
||||
alert = .leaveGroupAlert
|
||||
} label: {
|
||||
Label("Leave group", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
Label(label, systemImage: "rectangle.portrait.and.arrow.right")
|
||||
.foregroundColor(Color.red)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO reuse this and clearChatAlert with ChatInfoView
|
||||
private func deleteGroupAlert() -> Alert {
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
|
||||
return Alert(
|
||||
title: Text("Delete group?"),
|
||||
message: deleteGroupAlertMessage(),
|
||||
title: Text(label),
|
||||
message: deleteGroupAlertMessage(groupInfo),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
Task {
|
||||
do {
|
||||
@@ -537,10 +552,6 @@ struct GroupChatInfoView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func deleteGroupAlertMessage() -> Text {
|
||||
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
|
||||
}
|
||||
|
||||
private func clearChatAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Clear conversation?"),
|
||||
@@ -556,9 +567,15 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
private func leaveGroupAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Leave group?"),
|
||||
message: Text("You will stop receiving messages from this group. Chat history will be preserved."),
|
||||
let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?"
|
||||
let messageLabel: LocalizedStringKey = (
|
||||
groupInfo.businessChat == nil
|
||||
? "You will stop receiving messages from this group. Chat history will be preserved."
|
||||
: "You will stop receiving messages from this chat. Chat history will be preserved."
|
||||
)
|
||||
return Alert(
|
||||
title: Text(titleLabel),
|
||||
message: Text(messageLabel),
|
||||
primaryButton: .destructive(Text("Leave")) {
|
||||
Task {
|
||||
await leaveGroup(chat.chatInfo.apiId)
|
||||
@@ -602,9 +619,14 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
|
||||
Alert(
|
||||
let messageLabel: LocalizedStringKey = (
|
||||
groupInfo.businessChat == nil
|
||||
? "Member will be removed from group - this cannot be undone!"
|
||||
: "Member will be removed from chat - this cannot be undone!"
|
||||
)
|
||||
return Alert(
|
||||
title: Text("Remove member?"),
|
||||
message: Text("Member will be removed from group - this cannot be undone!"),
|
||||
message: Text(messageLabel),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
Task {
|
||||
do {
|
||||
@@ -624,26 +646,80 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View {
|
||||
NavigationLink {
|
||||
GroupPreferencesView(
|
||||
groupInfo: groupInfo,
|
||||
preferences: groupInfo.wrappedValue.fullGroupPreferences,
|
||||
currentPreferences: groupInfo.wrappedValue.fullGroupPreferences,
|
||||
creatingGroup: creatingGroup
|
||||
)
|
||||
.navigationBarTitle("Group preferences")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
if creatingGroup {
|
||||
Text("Set group preferences")
|
||||
} else {
|
||||
Label("Group preferences", systemImage: "switch.2")
|
||||
func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
|
||||
groupInfo.businessChat == nil ? (
|
||||
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
|
||||
) : (
|
||||
groupInfo.membership.memberCurrent ? Text("Chat will be deleted for all members - this cannot be undone!") : Text("Chat will be deleted for you - this cannot be undone!")
|
||||
)
|
||||
}
|
||||
|
||||
struct GroupPreferencesButton: View {
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State var preferences: FullGroupPreferences
|
||||
@State var currentPreferences: FullGroupPreferences
|
||||
var creatingGroup: Bool = false
|
||||
|
||||
private var label: LocalizedStringKey {
|
||||
groupInfo.businessChat == nil ? "Group preferences" : "Chat preferences"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationLink {
|
||||
GroupPreferencesView(
|
||||
groupInfo: $groupInfo,
|
||||
preferences: $preferences,
|
||||
currentPreferences: currentPreferences,
|
||||
creatingGroup: creatingGroup,
|
||||
savePreferences: savePreferences
|
||||
)
|
||||
.navigationBarTitle(label)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.onDisappear {
|
||||
let saveText = NSLocalizedString(
|
||||
creatingGroup ? "Save" : "Save and notify group members",
|
||||
comment: "alert button"
|
||||
)
|
||||
|
||||
if groupInfo.fullGroupPreferences != preferences {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Save preferences?", comment: "alert title"),
|
||||
buttonTitle: saveText,
|
||||
buttonAction: { savePreferences() },
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if creatingGroup {
|
||||
Text("Set group preferences")
|
||||
} else {
|
||||
Label(label, systemImage: "switch.2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func savePreferences() {
|
||||
Task {
|
||||
do {
|
||||
var gp = groupInfo.groupProfile
|
||||
gp.groupPreferences = toGroupPreferences(preferences)
|
||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
|
||||
await MainActor.run {
|
||||
groupInfo = gInfo
|
||||
ChatModel.shared.updateGroup(gInfo)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
} catch {
|
||||
logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func cantInviteIncognitoAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Can't invite contacts!"),
|
||||
|
||||
@@ -14,11 +14,15 @@ struct GroupMemberInfoView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State var groupInfo: GroupInfo
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var groupMember: GMember
|
||||
var navigation: Bool = false
|
||||
@State private var connectionStats: ConnectionStats? = nil
|
||||
@State private var connectionCode: String? = nil
|
||||
@State private var connectionLoaded: Bool = false
|
||||
@State private var knownContactChat: Chat? = nil
|
||||
@State private var knownContact: Contact? = nil
|
||||
@State private var knownContactConnectionStats: ConnectionStats? = nil
|
||||
@State private var newRole: GroupMemberRole = .member
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
@@ -118,8 +122,8 @@ struct GroupMemberInfoView: View {
|
||||
} label: {
|
||||
Label("Share address", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
if let contactId = member.memberContactId {
|
||||
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||
if member.memberContactId != nil {
|
||||
if knownContactChat == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||
connectViaAddressButton(contactLink)
|
||||
}
|
||||
} else {
|
||||
@@ -135,7 +139,8 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
|
||||
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Group" : "Chat"
|
||||
infoRow(label, groupInfo.displayName)
|
||||
|
||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
Picker("Change role", selection: $newRole) {
|
||||
@@ -227,6 +232,18 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
|
||||
}
|
||||
if let contactId = member.memberContactId, let (contactChat, contact) = knownDirectChat(contactId) {
|
||||
knownContactChat = contactChat
|
||||
knownContact = contact
|
||||
do {
|
||||
let (stats, _) = try await apiContactInfo(contactChat.chatInfo.apiId)
|
||||
await MainActor.run {
|
||||
knownContactConnectionStats = stats
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiContactInfo error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: newRole) { newRole in
|
||||
if newRole != member.memberRole {
|
||||
@@ -260,6 +277,11 @@ struct GroupMemberInfoView: View {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
.onChange(of: chat.chatInfo) { c in
|
||||
if case let .group(gI) = chat.chatInfo {
|
||||
groupInfo = gI
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
|
||||
@@ -267,10 +289,10 @@ struct GroupMemberInfoView: View {
|
||||
GeometryReader { g in
|
||||
let buttonWidth = g.size.width / 4
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
if let contactId = member.memberContactId, let (chat, contact) = knownDirectChat(contactId) {
|
||||
if let chat = knownContactChat, let contact = knownContact {
|
||||
knownDirectChatButton(chat, width: buttonWidth)
|
||||
AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
AudioCallButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
VideoButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
|
||||
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||
if let contactId = member.memberContactId {
|
||||
newDirectChatButton(contactId, width: buttonWidth)
|
||||
@@ -305,10 +327,15 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
|
||||
func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) {
|
||||
let messageLabel: LocalizedStringKey = (
|
||||
groupInfo.businessChat == nil
|
||||
? "Direct messages between members are prohibited."
|
||||
: "Direct messages between members are prohibited in this chat."
|
||||
)
|
||||
alert = .someAlert(alert: SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: title,
|
||||
message: "Direct messages between members are prohibited in this group."
|
||||
message: messageLabel
|
||||
),
|
||||
id: "can't message member, direct messages prohibited"
|
||||
))
|
||||
@@ -354,25 +381,49 @@ struct GroupMemberInfoView: View {
|
||||
|
||||
func createMemberContactButton(width: CGFloat) -> some View {
|
||||
InfoViewButton(image: "message.fill", title: "message", width: width) {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
|
||||
await MainActor.run {
|
||||
progressIndicator = false
|
||||
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
|
||||
ItemsModel.shared.loadOpenChat(memberContact.id) {
|
||||
dismissAllSheets(animated: true)
|
||||
if let connStats = connectionStats {
|
||||
if connStats.ratchetSyncState == .ok {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
|
||||
await MainActor.run {
|
||||
progressIndicator = false
|
||||
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
|
||||
ItemsModel.shared.loadOpenChat(memberContact.id) {
|
||||
dismissAllSheets(animated: true)
|
||||
}
|
||||
NetworkModel.shared.setContactNetworkStatus(memberContact, .connected)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error creating member contact")
|
||||
await MainActor.run {
|
||||
progressIndicator = false
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
NetworkModel.shared.setContactNetworkStatus(memberContact, .connected)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error creating member contact")
|
||||
await MainActor.run {
|
||||
progressIndicator = false
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
} else if connStats.ratchetSyncAllowed {
|
||||
alert = .someAlert(alert: SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Fix connection?"),
|
||||
message: Text("Connection requires encryption renegotiation."),
|
||||
primaryButton: .default(Text("Fix")) {
|
||||
syncMemberConnection(force: false)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
),
|
||||
id: "can't message member, fix connection"
|
||||
))
|
||||
} else {
|
||||
alert = .someAlert(alert: SomeAlert(
|
||||
alert: mkAlert(
|
||||
title: "Can't message member",
|
||||
message: "Encryption renegotiation in progress."
|
||||
),
|
||||
id: "can't message contact, encryption renegotiation in progress"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,7 +439,7 @@ struct GroupMemberInfoView: View {
|
||||
Text(Image(systemName: "checkmark.shield"))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.font(.title2)
|
||||
+ Text(" ")
|
||||
+ textSpace
|
||||
+ Text(mem.displayName)
|
||||
.font(.largeTitle)
|
||||
)
|
||||
@@ -537,9 +588,14 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
|
||||
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
|
||||
Alert(
|
||||
let label: LocalizedStringKey = (
|
||||
groupInfo.businessChat == nil
|
||||
? "Member will be removed from group - this cannot be undone!"
|
||||
: "Member will be removed from chat - this cannot be undone!"
|
||||
)
|
||||
return Alert(
|
||||
title: Text("Remove member?"),
|
||||
message: Text("Member will be removed from group - this cannot be undone!"),
|
||||
message: Text(label),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
Task {
|
||||
do {
|
||||
@@ -562,7 +618,15 @@ struct GroupMemberInfoView: View {
|
||||
private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert {
|
||||
Alert(
|
||||
title: Text("Change member role?"),
|
||||
message: mem.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."),
|
||||
message: (
|
||||
mem.memberCurrent
|
||||
? (
|
||||
groupInfo.businessChat == nil
|
||||
? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.")
|
||||
: Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.")
|
||||
)
|
||||
: Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.")
|
||||
),
|
||||
primaryButton: .default(Text("Change")) {
|
||||
Task {
|
||||
do {
|
||||
@@ -570,7 +634,7 @@ struct GroupMemberInfoView: View {
|
||||
await MainActor.run {
|
||||
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
|
||||
|
||||
} catch let error {
|
||||
newRole = mem.memberRole
|
||||
logger.error("apiMemberRole error: \(responseError(error))")
|
||||
@@ -739,6 +803,7 @@ struct GroupMemberInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupMemberInfoView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
chat: Chat.sampleData,
|
||||
groupMember: GMember.sampleData
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ struct GroupPreferencesView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State var preferences: FullGroupPreferences
|
||||
@State var currentPreferences: FullGroupPreferences
|
||||
@Binding var preferences: FullGroupPreferences
|
||||
var currentPreferences: FullGroupPreferences
|
||||
let creatingGroup: Bool
|
||||
let savePreferences: () -> Void
|
||||
@State private var showSaveDialogue = false
|
||||
|
||||
var body: some View {
|
||||
@@ -38,7 +39,7 @@ struct GroupPreferencesView: View {
|
||||
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||
featureSection(.history, $preferences.history.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
if groupInfo.isOwner {
|
||||
Section {
|
||||
Button("Reset") { preferences = currentPreferences }
|
||||
Button(saveText) { savePreferences() }
|
||||
@@ -68,7 +69,10 @@ struct GroupPreferencesView: View {
|
||||
savePreferences()
|
||||
dismiss()
|
||||
}
|
||||
Button("Exit without saving") { dismiss() }
|
||||
Button("Exit without saving") {
|
||||
preferences = currentPreferences
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +81,7 @@ struct GroupPreferencesView: View {
|
||||
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
|
||||
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
|
||||
let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on
|
||||
if groupInfo.canEdit {
|
||||
if groupInfo.isOwner {
|
||||
let enable = Binding(
|
||||
get: { enableFeature.wrappedValue == .on },
|
||||
set: { on, _ in enableFeature.wrappedValue = on ? .on : .off }
|
||||
@@ -123,7 +127,7 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
|
||||
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.onChange(of: enableFeature.wrappedValue) { enabled in
|
||||
@@ -132,32 +136,16 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func savePreferences() {
|
||||
Task {
|
||||
do {
|
||||
var gp = groupInfo.groupProfile
|
||||
gp.groupPreferences = toGroupPreferences(preferences)
|
||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
|
||||
await MainActor.run {
|
||||
groupInfo = gInfo
|
||||
chatModel.updateGroup(gInfo)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
} catch {
|
||||
logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupPreferencesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupPreferencesView(
|
||||
groupInfo: Binding.constant(GroupInfo.sampleData),
|
||||
preferences: FullGroupPreferences.sampleData,
|
||||
preferences: Binding.constant(FullGroupPreferences.sampleData),
|
||||
currentPreferences: FullGroupPreferences.sampleData,
|
||||
creatingGroup: false
|
||||
creatingGroup: false,
|
||||
savePreferences: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ struct GroupWelcomeView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if groupInfo.canEdit {
|
||||
if groupInfo.isOwner && groupInfo.businessChat == nil {
|
||||
editorView()
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if welcomeTextUnchanged() {
|
||||
|
||||
@@ -42,7 +42,8 @@ struct ChatHelp: View {
|
||||
Text("above, then choose:")
|
||||
}
|
||||
|
||||
Text("**Add contact**: to create a new invitation link, or connect via a link you received.")
|
||||
Text("**Create 1-time link**: to create and share a new invitation link.")
|
||||
Text("**Scan / Paste link**: to connect via a link you received.")
|
||||
Text("**Create group**: to create a new group.")
|
||||
}
|
||||
.padding(.top, 24)
|
||||
|
||||
@@ -404,8 +404,9 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
|
||||
private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||
Alert(
|
||||
title: Text("Delete group?"),
|
||||
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
|
||||
return Alert(
|
||||
title: Text(label),
|
||||
message: deleteGroupAlertMessage(groupInfo),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
Task { await deleteChat(chat) }
|
||||
@@ -414,10 +415,6 @@ struct ChatListNavLink: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
|
||||
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
|
||||
}
|
||||
|
||||
private func clearChatAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Clear conversation?"),
|
||||
@@ -441,9 +438,15 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
|
||||
private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||
Alert(
|
||||
title: Text("Leave group?"),
|
||||
message: Text("You will stop receiving messages from this group. Chat history will be preserved."),
|
||||
let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?"
|
||||
let messageLabel: LocalizedStringKey = (
|
||||
groupInfo.businessChat == nil
|
||||
? "You will stop receiving messages from this group. Chat history will be preserved."
|
||||
: "You will stop receiving messages from this chat. Chat history will be preserved."
|
||||
)
|
||||
return Alert(
|
||||
title: Text(titleLabel),
|
||||
message: Text(messageLabel),
|
||||
primaryButton: .destructive(Text("Leave")) {
|
||||
Task { await leaveGroup(groupInfo.groupId) }
|
||||
},
|
||||
|
||||
@@ -172,7 +172,7 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
private var verifiedIcon: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
@@ -232,12 +232,12 @@ struct ChatPreviewView: View {
|
||||
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||
|
||||
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
||||
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
|
||||
Text(Image(systemName: s)).foregroundColor(color) + textSpace
|
||||
}
|
||||
|
||||
func attachment() -> Text {
|
||||
switch draft.preview {
|
||||
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ")
|
||||
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + textSpace
|
||||
case .mediaPreviews: return image("photo")
|
||||
case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration))
|
||||
default: return Text("")
|
||||
@@ -367,11 +367,11 @@ struct ChatPreviewView: View {
|
||||
case .sndErrorAuth, .sndError:
|
||||
return Text(Image(systemName: "multiply"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.red) + Text(" ")
|
||||
.foregroundColor(.red) + textSpace
|
||||
case .sndWarning:
|
||||
return Text(Image(systemName: "exclamationmark.triangle.fill"))
|
||||
.font(.caption)
|
||||
.foregroundColor(.orange) + Text(" ")
|
||||
.foregroundColor(.orange) + textSpace
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,7 +587,7 @@ struct SMPStatsView: View {
|
||||
} header: {
|
||||
Text("Statistics")
|
||||
} footer: {
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,7 +703,7 @@ struct XFTPStatsView: View {
|
||||
} header: {
|
||||
Text("Statistics")
|
||||
} footer: {
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
|
||||
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ struct ContactListNavLink: View {
|
||||
}
|
||||
|
||||
private var verifiedIcon: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
||||
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||
.foregroundColor(.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
//
|
||||
// ChatArchiveView.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 23/06/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatArchiveView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var archiveName: String
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
||||
@State private var showDeleteAlert = false
|
||||
|
||||
var body: some View {
|
||||
let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName)
|
||||
let fileTs = chatArchiveTimeDefault.get()
|
||||
List {
|
||||
Section {
|
||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||
Button {
|
||||
showShareSheet(items: [fileUrl])
|
||||
} label: {
|
||||
Text("Save archive")
|
||||
}
|
||||
}
|
||||
settingsRow("trash", color: theme.colors.secondary) {
|
||||
Button {
|
||||
showDeleteAlert = true
|
||||
} label: {
|
||||
Text("Delete archive").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Chat archive")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("Created on \(fileTs)")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showDeleteAlert) {
|
||||
Alert(
|
||||
title: Text("Delete chat archive?"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: fileUrl.path)
|
||||
chatArchiveName = nil
|
||||
chatArchiveTime = 0
|
||||
} catch let error {
|
||||
logger.error("removeItem error \(String(describing: error))")
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatArchiveView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatArchiveView(archiveName: "")
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ struct DatabaseEncryptionView: View {
|
||||
@State private var confirmNewKey = ""
|
||||
@State private var currentKeyShown = false
|
||||
|
||||
let stopChatRunBlockStartChat: (Binding<Bool>, @escaping () async throws -> Bool) -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
List {
|
||||
@@ -134,46 +136,61 @@ struct DatabaseEncryptionView: View {
|
||||
.onAppear {
|
||||
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
||||
}
|
||||
.disabled(m.chatRunning != false)
|
||||
.disabled(progressIndicator)
|
||||
.alert(item: $alert) { item in databaseEncryptionAlert(item) }
|
||||
}
|
||||
|
||||
private func encryptDatabase() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
encryptionStartedDefault.set(true)
|
||||
encryptionStartedAtDefault.set(Date.now)
|
||||
if !m.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||
encryptionStartedDefault.set(false)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if migration {
|
||||
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||
}
|
||||
if useKeychain {
|
||||
if kcDatabasePassword.set(newKey) {
|
||||
await resetFormAfterEncryption(true)
|
||||
await operationEnded(.databaseEncrypted)
|
||||
} else {
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||
}
|
||||
} else {
|
||||
if migration {
|
||||
removePassphraseFromKeyChain()
|
||||
}
|
||||
await resetFormAfterEncryption()
|
||||
private func encryptDatabaseAsync() async -> Bool {
|
||||
await MainActor.run {
|
||||
progressIndicator = true
|
||||
}
|
||||
do {
|
||||
encryptionStartedDefault.set(true)
|
||||
encryptionStartedAtDefault.set(Date.now)
|
||||
if !m.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||
encryptionStartedDefault.set(false)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if migration {
|
||||
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||
}
|
||||
if useKeychain {
|
||||
if kcDatabasePassword.set(newKey) {
|
||||
await resetFormAfterEncryption(true)
|
||||
await operationEnded(.databaseEncrypted)
|
||||
}
|
||||
} catch let error {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||
}
|
||||
} else {
|
||||
if migration {
|
||||
removePassphraseFromKeyChain()
|
||||
}
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.databaseEncrypted)
|
||||
}
|
||||
return true
|
||||
} catch let error {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func encryptDatabase() {
|
||||
// it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped
|
||||
if migration {
|
||||
Task {
|
||||
await encryptDatabaseAsync()
|
||||
}
|
||||
} else {
|
||||
stopChatRunBlockStartChat($progressIndicator) {
|
||||
return await encryptDatabaseAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,6 +388,6 @@ func validKey(_ s: String) -> Bool {
|
||||
|
||||
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false)
|
||||
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false, stopChatRunBlockStartChat: { _, _ in true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct DatabaseErrorView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State var status: DBMigrationResult
|
||||
@State private var dbKey = ""
|
||||
@State private var storedDBKey = kcDatabasePassword.get()
|
||||
@@ -28,23 +29,39 @@ struct DatabaseErrorView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func databaseErrorView() -> some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
switch status {
|
||||
case let .errorNotADatabase(dbFile):
|
||||
if useKeychain && storedDBKey != nil && storedDBKey != "" {
|
||||
titleText("Wrong database passphrase")
|
||||
Text("Database passphrase is different from saved in the keychain.")
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 25)
|
||||
|
||||
databaseKeyField(onSubmit: saveAndRunChat)
|
||||
saveAndOpenButton()
|
||||
fileNameText(dbFile)
|
||||
Spacer()
|
||||
VStack(spacing: 10) {
|
||||
saveAndOpenButton()
|
||||
fileNameText(dbFile)
|
||||
}
|
||||
} else {
|
||||
titleText("Encrypted database")
|
||||
Text("Database passphrase is required to open chat.")
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
if useKeychain {
|
||||
databaseKeyField(onSubmit: saveAndRunChat)
|
||||
Spacer()
|
||||
saveAndOpenButton()
|
||||
} else {
|
||||
databaseKeyField(onSubmit: { runChat() })
|
||||
Spacer()
|
||||
openChatButton()
|
||||
}
|
||||
}
|
||||
@@ -52,73 +69,105 @@ struct DatabaseErrorView: View {
|
||||
switch migrationError {
|
||||
case let .upgrade(upMigrations):
|
||||
titleText("Database upgrade")
|
||||
Button("Upgrade and open chat") { runChat(confirmMigrations: .yesUp) }
|
||||
fileNameText(dbFile)
|
||||
migrationsText(upMigrations.map(\.upName))
|
||||
Spacer()
|
||||
VStack(spacing: 10) {
|
||||
Button("Upgrade and open chat") {
|
||||
runChat(confirmMigrations: .yesUp)
|
||||
}.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
fileNameText(dbFile)
|
||||
}
|
||||
case let .downgrade(downMigrations):
|
||||
titleText("Database downgrade")
|
||||
Text("Warning: you may lose some data!").bold()
|
||||
Button("Downgrade and open chat") { runChat(confirmMigrations: .yesUpDown) }
|
||||
fileNameText(dbFile)
|
||||
Text("Warning: you may lose some data!")
|
||||
.bold()
|
||||
.padding(.horizontal, 25)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
migrationsText(downMigrations)
|
||||
Spacer()
|
||||
VStack(spacing: 10) {
|
||||
Button("Downgrade and open chat") {
|
||||
runChat(confirmMigrations: .yesUpDown)
|
||||
}.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
fileNameText(dbFile)
|
||||
}
|
||||
case let .migrationError(mtrError):
|
||||
titleText("Incompatible database version")
|
||||
fileNameText(dbFile)
|
||||
Text("Error: ") + Text(mtrErrorDescription(mtrError))
|
||||
fileNameText(dbFile, font: .callout)
|
||||
errorView(Text(mtrErrorDescription(mtrError)))
|
||||
}
|
||||
case let .errorSQL(dbFile, migrationSQLError):
|
||||
titleText("Database error")
|
||||
fileNameText(dbFile)
|
||||
Text("Error: \(migrationSQLError)")
|
||||
fileNameText(dbFile, font: .callout)
|
||||
errorView(Text("Error: \(migrationSQLError)"))
|
||||
case .errorKeychain:
|
||||
titleText("Keychain error")
|
||||
Text("Cannot access keychain to save database password")
|
||||
errorView(Text("Cannot access keychain to save database password"))
|
||||
case .invalidConfirmation:
|
||||
// this can only happen if incorrect parameter is passed
|
||||
Text(String("Invalid migration confirmation")).font(.title)
|
||||
titleText("Invalid migration confirmation")
|
||||
errorView()
|
||||
|
||||
case let .unknown(json):
|
||||
titleText("Database error")
|
||||
Text("Unknown database error: \(json)")
|
||||
errorView(Text("Unknown database error: \(json)"))
|
||||
case .ok:
|
||||
EmptyView()
|
||||
}
|
||||
if showRestoreDbButton {
|
||||
Spacer().frame(height: 10)
|
||||
Spacer()
|
||||
Text("The attempt to change database passphrase was not completed.")
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 25)
|
||||
.font(.footnote)
|
||||
|
||||
restoreDbButton()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 75)
|
||||
.padding(.bottom, 25)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() }
|
||||
}
|
||||
|
||||
private func titleText(_ s: LocalizedStringKey) -> Text {
|
||||
Text(s).font(.title)
|
||||
private func titleText(_ s: LocalizedStringKey) -> some View {
|
||||
Text(s).font(.largeTitle).bold().multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
private func fileNameText(_ f: String) -> Text {
|
||||
Text("File: \((f as NSString).lastPathComponent)")
|
||||
private func fileNameText(_ f: String, font: Font = .caption) -> Text {
|
||||
Text("File: \((f as NSString).lastPathComponent)").font(font)
|
||||
}
|
||||
|
||||
private func migrationsText(_ ms: [String]) -> Text {
|
||||
Text("Migrations: \(ms.joined(separator: ", "))")
|
||||
private func migrationsText(_ ms: [String]) -> some View {
|
||||
(Text("Migrations:").font(.subheadline) + Text(verbatim: "\n") + Text(ms.joined(separator: "\n")).font(.caption))
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 25)
|
||||
}
|
||||
|
||||
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
|
||||
PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color(uiColor: .tertiarySystemFill))
|
||||
)
|
||||
}
|
||||
|
||||
private func saveAndOpenButton() -> some View {
|
||||
Button("Save passphrase and open chat") {
|
||||
saveAndRunChat()
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
}
|
||||
|
||||
private func openChatButton() -> some View {
|
||||
Button("Open chat") {
|
||||
runChat()
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
}
|
||||
|
||||
private func saveAndRunChat() {
|
||||
@@ -192,8 +241,9 @@ struct DatabaseErrorView: View {
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
} label: {
|
||||
Text("Restore database backup").foregroundColor(.red)
|
||||
Text("Restore database backup")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
}
|
||||
|
||||
private func restoreDb() {
|
||||
@@ -208,6 +258,23 @@ struct DatabaseErrorView: View {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func errorView(_ s: Text? = nil) -> some View {
|
||||
VStack(spacing: 35) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.foregroundColor(.red)
|
||||
|
||||
if let text = s {
|
||||
text
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct DatabaseErrorView_Previews: PreviewProvider {
|
||||
|
||||
@@ -46,6 +46,7 @@ struct DatabaseView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let dismissSettingsSheet: DismissAction
|
||||
@State private var runChat = false
|
||||
@State private var stoppingChat = false
|
||||
@State private var alert: DatabaseAlert? = nil
|
||||
@State private var showFileImporter = false
|
||||
@State private var importedArchivePath: URL?
|
||||
@@ -57,6 +58,8 @@ struct DatabaseView: View {
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var appFilesCountAndSize: (Int, Int)?
|
||||
|
||||
@State private var showDatabaseEncryptionView = false
|
||||
|
||||
@State var chatItemTTL: ChatItemTTL
|
||||
@State private var currentChatItemTTL: ChatItemTTL = .none
|
||||
|
||||
@@ -69,7 +72,20 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func chatDatabaseView() -> some View {
|
||||
NavigationLink(isActive: $showDatabaseEncryptionView) {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
|
||||
stopChatRunBlockStartChat(false, progressIndicator, block)
|
||||
})
|
||||
.navigationTitle("Database passphrase")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
|
||||
List {
|
||||
let stopped = m.chatRunning == false
|
||||
Section {
|
||||
@@ -101,9 +117,10 @@ struct DatabaseView: View {
|
||||
isOn: $runChat
|
||||
)
|
||||
.onChange(of: runChat) { _ in
|
||||
if (runChat) {
|
||||
startChat()
|
||||
} else {
|
||||
if runChat {
|
||||
DatabaseView.startChat($runChat, $progressIndicator)
|
||||
} else if !stoppingChat {
|
||||
stoppingChat = false
|
||||
alert = .stopChat
|
||||
}
|
||||
}
|
||||
@@ -123,7 +140,9 @@ struct DatabaseView: View {
|
||||
let color: Color = unencrypted ? .orange : theme.colors.secondary
|
||||
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
||||
NavigationLink {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
|
||||
stopChatRunBlockStartChat(false, progressIndicator, block)
|
||||
})
|
||||
.navigationTitle("Database passphrase")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
@@ -133,9 +152,14 @@ struct DatabaseView: View {
|
||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||
Button("Export database") {
|
||||
if initialRandomDBPassphraseGroupDefault.get() && !unencrypted {
|
||||
alert = .exportProhibited
|
||||
showDatabaseEncryptionView = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
alert = .exportProhibited
|
||||
}
|
||||
} else {
|
||||
exportArchive()
|
||||
stopChatRunBlockStartChat(stopped, $progressIndicator) {
|
||||
await exportArchive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,20 +168,6 @@ struct DatabaseView: View {
|
||||
showFileImporter = true
|
||||
}
|
||||
}
|
||||
if let archiveName = chatArchiveName {
|
||||
let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get()
|
||||
? "Old database archive"
|
||||
: "New database archive"
|
||||
settingsRow("archivebox", color: theme.colors.secondary) {
|
||||
NavigationLink {
|
||||
ChatArchiveView(archiveName: archiveName)
|
||||
.navigationTitle(title)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRow("trash.slash", color: theme.colors.secondary) {
|
||||
Button("Delete database", role: .destructive) {
|
||||
alert = .deleteChat
|
||||
@@ -167,14 +177,10 @@ struct DatabaseView: View {
|
||||
Text("Chat database")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text(
|
||||
stopped
|
||||
? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts."
|
||||
: "Stop chat to enable database actions"
|
||||
)
|
||||
Text("You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.disabled(!stopped)
|
||||
.disabled(progressIndicator)
|
||||
|
||||
if case .group = dbContainer, legacyDatabase {
|
||||
Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) {
|
||||
@@ -190,7 +196,7 @@ struct DatabaseView: View {
|
||||
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
|
||||
alert = .deleteFilesAndMedia
|
||||
}
|
||||
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
|
||||
.disabled(progressIndicator || appFilesCountAndSize?.0 == 0)
|
||||
} header: {
|
||||
Text("Files & media")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
@@ -255,7 +261,10 @@ struct DatabaseView: View {
|
||||
title: Text("Import chat database?"),
|
||||
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||
primaryButton: .destructive(Text("Import")) {
|
||||
importArchive(fileURL)
|
||||
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
|
||||
_ = await DatabaseView.importArchive(fileURL, $progressIndicator, $alert)
|
||||
return true
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
@@ -263,19 +272,15 @@ struct DatabaseView: View {
|
||||
return Alert(title: Text("Error: no database file"))
|
||||
}
|
||||
case .archiveImported:
|
||||
return Alert(
|
||||
title: Text("Chat database imported"),
|
||||
message: Text("Restart the app to use imported chat database")
|
||||
)
|
||||
let (title, message) = archiveImportedAlertText()
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .archiveImportedWithErrors(errs):
|
||||
return Alert(
|
||||
title: Text("Chat database imported"),
|
||||
message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
|
||||
)
|
||||
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .archiveExportedWithErrors(archivePath, errs):
|
||||
return Alert(
|
||||
title: Text("Chat database exported"),
|
||||
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
||||
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
|
||||
dismissButton: .default(Text("Continue")) {
|
||||
showShareSheet(items: [archivePath])
|
||||
}
|
||||
@@ -285,15 +290,17 @@ struct DatabaseView: View {
|
||||
title: Text("Delete chat profile?"),
|
||||
message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deleteChat()
|
||||
let wasStopped = m.chatRunning == false
|
||||
stopChatRunBlockStartChat(wasStopped, $progressIndicator) {
|
||||
_ = await deleteChat()
|
||||
return true
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .chatDeleted:
|
||||
return Alert(
|
||||
title: Text("Chat database deleted"),
|
||||
message: Text("Restart the app to create a new chat profile")
|
||||
)
|
||||
let (title, message) = chatDeletedAlertText()
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case .deleteLegacyDatabase:
|
||||
return Alert(
|
||||
title: Text("Delete old database?"),
|
||||
@@ -308,7 +315,10 @@ struct DatabaseView: View {
|
||||
title: Text("Delete files and media?"),
|
||||
message: Text("This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deleteFiles()
|
||||
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
|
||||
deleteFiles()
|
||||
return true
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
@@ -328,95 +338,180 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func authStopChat() {
|
||||
private func authStopChat(_ onStop: (() -> Void)? = nil) {
|
||||
if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) {
|
||||
authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success: stopChat()
|
||||
case .unavailable: stopChat()
|
||||
case .success: stopChat(onStop)
|
||||
case .unavailable: stopChat(onStop)
|
||||
case .failed: withAnimation { runChat = true }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopChat()
|
||||
stopChat(onStop)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopChat() {
|
||||
private func stopChat(_ onStop: (() -> Void)? = nil) {
|
||||
Task {
|
||||
do {
|
||||
try await stopChatAsync()
|
||||
onStop?()
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
runChat = true
|
||||
alert = .error(title: "Error stopping chat", error: responseError(error))
|
||||
showAlert("Error stopping chat", message: responseError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchive() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let (archivePath, archiveErrors) = try await exportChatArchive()
|
||||
if archiveErrors.isEmpty {
|
||||
showShareSheet(items: [archivePath])
|
||||
await MainActor.run { progressIndicator = false }
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
|
||||
progressIndicator = false
|
||||
func stopChatRunBlockStartChat(
|
||||
_ stopped: Bool,
|
||||
_ progressIndicator: Binding<Bool>,
|
||||
_ block: @escaping () async throws -> Bool
|
||||
) {
|
||||
// if the chat was running, the sequence is: stop chat, run block, start chat.
|
||||
// Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not
|
||||
if stopped {
|
||||
Task {
|
||||
do {
|
||||
_ = try await block()
|
||||
} catch {
|
||||
logger.error("Error while executing block: \(error)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
authStopChat {
|
||||
stoppingChat = true
|
||||
runChat = false
|
||||
Task {
|
||||
// if it throws, let's start chat again anyway
|
||||
var canStart = false
|
||||
do {
|
||||
canStart = try await block()
|
||||
} catch {
|
||||
logger.error("Error executing block: \(error)")
|
||||
canStart = true
|
||||
}
|
||||
if canStart {
|
||||
await MainActor.run {
|
||||
DatabaseView.startChat($runChat, $progressIndicator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func startChat(_ runChat: Binding<Bool>, _ progressIndicator: Binding<Bool>) {
|
||||
progressIndicator.wrappedValue = true
|
||||
let m = ChatModel.shared
|
||||
if m.chatDbChanged {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resetChatCtrl()
|
||||
do {
|
||||
let hadDatabase = hasDatabase()
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
AppChatState.shared.set(.active)
|
||||
if m.chatDbStatus != .ok || !hadDatabase {
|
||||
// Hide current view and show `DatabaseErrorView`
|
||||
dismissAllSheets(animated: true)
|
||||
}
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
progressIndicator.wrappedValue = false
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
_ = try apiStartChat()
|
||||
runChat.wrappedValue = true
|
||||
m.chatRunning = true
|
||||
ChatReceiver.shared.start()
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
runChat.wrappedValue = false
|
||||
showAlert(NSLocalizedString("Error starting chat", comment: ""), message: responseError(error))
|
||||
}
|
||||
progressIndicator.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchive() async -> Bool {
|
||||
await MainActor.run {
|
||||
progressIndicator = true
|
||||
}
|
||||
do {
|
||||
let (archivePath, archiveErrors) = try await exportChatArchive()
|
||||
if archiveErrors.isEmpty {
|
||||
showShareSheet(items: [archivePath])
|
||||
await MainActor.run { progressIndicator = false }
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
|
||||
progressIndicator = false
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||
progressIndicator = false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func importArchive(_ archivePath: URL) {
|
||||
static func importArchive(
|
||||
_ archivePath: URL,
|
||||
_ progressIndicator: Binding<Bool>,
|
||||
_ alert: Binding<DatabaseAlert?>
|
||||
) async -> Bool {
|
||||
if archivePath.startAccessingSecurityScopedResource() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
_ = kcDatabasePassword.remove()
|
||||
if archiveErrors.isEmpty {
|
||||
await operationEnded(.archiveImported)
|
||||
} else {
|
||||
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors))
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)))
|
||||
}
|
||||
archivePath.stopAccessingSecurityScopedResource()
|
||||
await MainActor.run {
|
||||
progressIndicator.wrappedValue = true
|
||||
}
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
shouldImportAppSettingsDefault.set(true)
|
||||
_ = kcDatabasePassword.remove()
|
||||
if archiveErrors.isEmpty {
|
||||
await operationEnded(.archiveImported, progressIndicator, alert)
|
||||
} else {
|
||||
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors), progressIndicator, alert)
|
||||
}
|
||||
return true
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert)
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert)
|
||||
}
|
||||
archivePath.stopAccessingSecurityScopedResource()
|
||||
} else {
|
||||
alert = .error(title: "Error accessing database file")
|
||||
showAlert("Error accessing database file")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func deleteChat() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
try await deleteChatAsync()
|
||||
await operationEnded(.chatDeleted)
|
||||
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting database", error: responseError(error)))
|
||||
}
|
||||
private func deleteChat() async -> Bool {
|
||||
await MainActor.run {
|
||||
progressIndicator = true
|
||||
}
|
||||
do {
|
||||
try await deleteChatAsync()
|
||||
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
||||
await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert)
|
||||
return true
|
||||
} catch let error {
|
||||
await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,39 +523,28 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func operationEnded(_ dbAlert: DatabaseAlert) async {
|
||||
private static func operationEnded(_ dbAlert: DatabaseAlert, _ progressIndicator: Binding<Bool>, _ alert: Binding<DatabaseAlert?>) async {
|
||||
await MainActor.run {
|
||||
let m = ChatModel.shared
|
||||
m.chatDbChanged = true
|
||||
m.chatInitialized = false
|
||||
progressIndicator = false
|
||||
alert = dbAlert
|
||||
progressIndicator.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
private func startChat() {
|
||||
if m.chatDbChanged {
|
||||
dismissSettingsSheet()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resetChatCtrl()
|
||||
do {
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
_ = try apiStartChat()
|
||||
runChat = true
|
||||
m.chatRunning = true
|
||||
ChatReceiver.shared.start()
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
runChat = false
|
||||
alert = .error(title: "Error starting chat", error: responseError(error))
|
||||
await withCheckedContinuation { cont in
|
||||
let okAlertActionWaiting = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default, handler: { _ in cont.resume() })
|
||||
// show these alerts globally so they are visible when all sheets will be hidden
|
||||
if case .archiveImported = dbAlert {
|
||||
let (title, message) = archiveImportedAlertText()
|
||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||
} else if case .archiveImportedWithErrors(let errs) = dbAlert {
|
||||
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
|
||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||
} else if case .chatDeleted = dbAlert {
|
||||
let (title, message) = chatDeletedAlertText()
|
||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||
} else {
|
||||
alert.wrappedValue = dbAlert
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,8 +587,28 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func archiveErrorsText(_ errs: [ArchiveError]) -> Text {
|
||||
return Text("\n" + errs.map(showArchiveError).joined(separator: "\n"))
|
||||
private func archiveImportedAlertText() -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database imported", comment: ""),
|
||||
NSLocalizedString("Restart the app to use imported chat database", comment: "")
|
||||
)
|
||||
}
|
||||
private func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database imported", comment: ""),
|
||||
NSLocalizedString("Restart the app to use imported chat database", comment: "") + "\n" + NSLocalizedString("Some non-fatal errors occurred during import:", comment: "") + archiveErrorsText(errs)
|
||||
)
|
||||
}
|
||||
|
||||
private func chatDeletedAlertText() -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database deleted", comment: ""),
|
||||
NSLocalizedString("Restart the app to create a new chat profile", comment: "")
|
||||
)
|
||||
}
|
||||
|
||||
func archiveErrorsText(_ errs: [ArchiveError]) -> String {
|
||||
return "\n" + errs.map(showArchiveError).joined(separator: "\n")
|
||||
|
||||
func showArchiveError(_ err: ArchiveError) -> String {
|
||||
switch err {
|
||||
|
||||
@@ -117,7 +117,7 @@ struct MigrateToAppGroupView: View {
|
||||
setV3DBMigration(.migration_error)
|
||||
migrationError = "Error starting chat: \(responseError(error))"
|
||||
}
|
||||
deleteOldArchive()
|
||||
deleteOldChatArchive()
|
||||
} label: {
|
||||
Text("Start chat")
|
||||
.font(.title)
|
||||
@@ -235,14 +235,16 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [Archive
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
let errs = try await apiExportArchive(config: config)
|
||||
if storagePath == nil {
|
||||
deleteOldArchive()
|
||||
deleteOldChatArchive()
|
||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||
chatArchiveTimeDefault.set(archiveTime)
|
||||
}
|
||||
return (archivePath, errs)
|
||||
}
|
||||
|
||||
func deleteOldArchive() {
|
||||
/// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then.
|
||||
/// Remove DEFAULT_CHAT_ARCHIVE_NAME and DEFAULT_CHAT_ARCHIVE_TIME as well
|
||||
func deleteOldChatArchive() {
|
||||
let d = UserDefaults.standard
|
||||
if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) {
|
||||
do {
|
||||
|
||||
@@ -177,7 +177,7 @@ struct MigrateFromDevice: View {
|
||||
case let .archiveExportedWithErrors(archivePath, errs):
|
||||
return Alert(
|
||||
title: Text("Chat database exported"),
|
||||
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
||||
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
|
||||
dismissButton: .default(Text("Continue")) {
|
||||
Task { await uploadArchive(path: archivePath) }
|
||||
}
|
||||
@@ -222,7 +222,8 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
|
||||
private func passphraseNotSetView() -> some View {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true)
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true, stopChatRunBlockStartChat: { _, _ in
|
||||
})
|
||||
.onChange(of: initialRandomDBPassphrase) { initial in
|
||||
if !initial {
|
||||
migrationState = .uploadConfirmation
|
||||
|
||||
@@ -103,6 +103,9 @@ struct MigrateToDevice: View {
|
||||
@State private var showQRCodeScanner: Bool = true
|
||||
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
|
||||
|
||||
@State private var importingArchiveFromFileProgressIndicator = false
|
||||
@State private var showFileImporter = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
switch migrationState {
|
||||
@@ -200,6 +203,12 @@ struct MigrateToDevice: View {
|
||||
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
|
||||
pasteLinkView()
|
||||
}
|
||||
Section(header: Text("Or import archive file").foregroundColor(theme.colors.secondary)) {
|
||||
archiveImportFromFileView()
|
||||
}
|
||||
}
|
||||
if importingArchiveFromFileProgressIndicator {
|
||||
progressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,6 +229,34 @@ struct MigrateToDevice: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
private func archiveImportFromFileView() -> some View {
|
||||
Button {
|
||||
showFileImporter = true
|
||||
} label: {
|
||||
Label("Import database", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(importingArchiveFromFileProgressIndicator)
|
||||
.fileImporter(
|
||||
isPresented: $showFileImporter,
|
||||
allowedContentTypes: [.zip],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
if case let .success(files) = result, let fileURL = files.first {
|
||||
Task {
|
||||
let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, Binding.constant(nil))
|
||||
if success {
|
||||
DatabaseView.startChat(
|
||||
Binding.constant(false),
|
||||
$importingArchiveFromFileProgressIndicator
|
||||
)
|
||||
hideView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func linkDownloadingView(_ link: String) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
|
||||
@@ -18,7 +18,6 @@ struct NewChatMenuButton: View {
|
||||
// @EnvironmentObject var chatModel: ChatModel
|
||||
@State private var showNewChatSheet = false
|
||||
@State private var alert: SomeAlert? = nil
|
||||
@State private var pendingConnection: PendingContactConnection? = nil
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
@@ -30,12 +29,8 @@ struct NewChatMenuButton: View {
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
.appSheet(isPresented: $showNewChatSheet) {
|
||||
NewChatSheet(pendingConnection: $pendingConnection)
|
||||
NewChatSheet()
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.onDisappear {
|
||||
alert = cleanupPendingConnection(contactConnection: pendingConnection)
|
||||
pendingConnection = nil
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
return a.alert
|
||||
@@ -55,7 +50,6 @@ struct NewChatSheet: View {
|
||||
@State private var searchShowingSimplexLink = false
|
||||
@State private var searchChatFilteredBySimplexLink: String? = nil
|
||||
@State private var alert: SomeAlert?
|
||||
@Binding var pendingConnection: PendingContactConnection?
|
||||
|
||||
// Sheet height management
|
||||
@State private var isAddContactActive = false
|
||||
@@ -110,17 +104,17 @@ struct NewChatSheet: View {
|
||||
if (searchText.isEmpty) {
|
||||
Section {
|
||||
NavigationLink(isActive: $isAddContactActive) {
|
||||
NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection)
|
||||
NewChatView(selection: .invite)
|
||||
.navigationTitle("New chat")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
navigateOnTap(Label("Add contact", systemImage: "link.badge.plus")) {
|
||||
navigateOnTap(Label("Create 1-time link", systemImage: "link.badge.plus")) {
|
||||
isAddContactActive = true
|
||||
}
|
||||
}
|
||||
NavigationLink(isActive: $isScanPasteLinkActive) {
|
||||
NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection)
|
||||
NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||
.navigationTitle("New chat")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
|
||||
@@ -45,32 +45,33 @@ enum NewChatOption: Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
func cleanupPendingConnection(contactConnection: PendingContactConnection?) -> SomeAlert? {
|
||||
var alert: SomeAlert? = nil
|
||||
|
||||
if !(ChatModel.shared.showingInvitation?.connChatUsed ?? true),
|
||||
let conn = contactConnection {
|
||||
alert = SomeAlert(
|
||||
alert: Alert(
|
||||
title: Text("Keep unused invitation?"),
|
||||
message: Text("You can view invitation link again in connection details."),
|
||||
primaryButton: .default(Text("Keep")) {},
|
||||
secondaryButton: .destructive(Text("Delete")) {
|
||||
Task {
|
||||
await deleteChat(Chat(
|
||||
chatInfo: .contactConnection(contactConnection: conn),
|
||||
chatItems: []
|
||||
))
|
||||
func showKeepInvitationAlert() {
|
||||
if let showingInvitation = ChatModel.shared.showingInvitation,
|
||||
!showingInvitation.connChatUsed {
|
||||
showAlert(
|
||||
NSLocalizedString("Keep unused invitation?", comment: "alert title"),
|
||||
message: NSLocalizedString("You can view invitation link again in connection details.", comment: "alert message"),
|
||||
actions: {[
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Keep", comment: "alert action"),
|
||||
style: .default
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Delete", comment: "alert action"),
|
||||
style: .destructive,
|
||||
handler: { _ in
|
||||
Task {
|
||||
await deleteChat(Chat(
|
||||
chatInfo: .contactConnection(contactConnection: showingInvitation.pcc),
|
||||
chatItems: []
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
id: "keepUnusedInvitation"
|
||||
)
|
||||
]}
|
||||
)
|
||||
}
|
||||
|
||||
ChatModel.shared.showingInvitation = nil
|
||||
|
||||
return alert
|
||||
}
|
||||
|
||||
struct NewChatView: View {
|
||||
@@ -84,13 +85,12 @@ struct NewChatView: View {
|
||||
@State var choosingProfile = false
|
||||
@State private var pastedLink: String = ""
|
||||
@State private var alert: NewChatViewAlert?
|
||||
@Binding var parentAlert: SomeAlert?
|
||||
@Binding var contactConnection: PendingContactConnection?
|
||||
@State private var contactConnection: PendingContactConnection? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Picker("New chat", selection: $selection) {
|
||||
Label("Add contact", systemImage: "link")
|
||||
Label("1-time link", systemImage: "link")
|
||||
.tag(NewChatOption.invite)
|
||||
Label("Connect via link", systemImage: "qrcode")
|
||||
.tag(NewChatOption.connect)
|
||||
@@ -157,7 +157,7 @@ struct NewChatView: View {
|
||||
}
|
||||
.onDisappear {
|
||||
if !choosingProfile {
|
||||
parentAlert = cleanupPendingConnection(contactConnection: contactConnection)
|
||||
showKeepInvitationAlert()
|
||||
contactConnection = nil
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,7 @@ struct NewChatView: View {
|
||||
if let (connReq, pcc) = r {
|
||||
await MainActor.run {
|
||||
m.updateContactConnection(pcc)
|
||||
m.showingInvitation = ShowingInvitation(connId: pcc.id, connChatUsed: false)
|
||||
m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false)
|
||||
connReqInvitation = connReq
|
||||
contactConnection = pcc
|
||||
}
|
||||
@@ -916,11 +916,17 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (
|
||||
)
|
||||
case let .groupLinkConnecting(_, groupInfo):
|
||||
if let groupInfo = groupInfo {
|
||||
return Alert(
|
||||
return groupInfo.businessChat == nil
|
||||
? Alert(
|
||||
title: Text("Group already exists!"),
|
||||
message: Text("You are already joining the group \(groupInfo.displayName)."),
|
||||
dismissButton: .default(Text("OK")) { cleanup?() }
|
||||
)
|
||||
: Alert(
|
||||
title: Text("Chat already exists!"),
|
||||
message: Text("You are already connecting to \(groupInfo.displayName)."),
|
||||
dismissButton: .default(Text("OK")) { cleanup?() }
|
||||
)
|
||||
} else {
|
||||
return Alert(
|
||||
title: Text("Already joining the group!"),
|
||||
@@ -1237,10 +1243,15 @@ func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
|
||||
}
|
||||
|
||||
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||
mkAlert(
|
||||
groupInfo.businessChat == nil
|
||||
? mkAlert(
|
||||
title: "Group already exists",
|
||||
message: "You are already in group \(groupInfo.displayName)."
|
||||
)
|
||||
: mkAlert(
|
||||
title: "Chat already exists",
|
||||
message: "You are already connected with \(groupInfo.displayName)."
|
||||
)
|
||||
}
|
||||
|
||||
enum ConnReqType: Equatable {
|
||||
@@ -1278,9 +1289,7 @@ struct NewChatView_Previews: PreviewProvider {
|
||||
@State var contactConnection: PendingContactConnection? = nil
|
||||
|
||||
NewChatView(
|
||||
selection: .invite,
|
||||
parentAlert: $parentAlert,
|
||||
contactConnection: $contactConnection
|
||||
selection: .invite
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,7 @@ struct AddressCreationCard: View {
|
||||
Text("Your SimpleX address")
|
||||
.font(.title3)
|
||||
Spacer()
|
||||
HStack(alignment: .center) {
|
||||
Text("How to use it")
|
||||
VStack {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
Text("How to use it") + textSpace + Text(Image(systemName: "info.circle")).foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -68,7 +62,7 @@ struct AddressCreationCard: View {
|
||||
.alert(isPresented: $showAddressCreationAlert) {
|
||||
Alert(
|
||||
title: Text("SimpleX address"),
|
||||
message: Text("You can create it in user picker."),
|
||||
message: Text("Tap Create SimpleX address in the menu to create it later."),
|
||||
dismissButton: .default(Text("Ok")) {
|
||||
withAnimation {
|
||||
addressCreationCardShown = true
|
||||
@@ -87,8 +81,8 @@ struct AddressCreationCard: View {
|
||||
.sheet(isPresented: $showAddressInfoSheet) {
|
||||
NavigationView {
|
||||
UserAddressLearnMore(showCreateAddressButton: true)
|
||||
.navigationTitle("SimpleX address")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationTitle("Address or 1-time link?")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ struct ChooseServerOperators: View {
|
||||
var onboarding: Bool
|
||||
@State private var serverOperators: [ServerOperator] = []
|
||||
@State private var selectedOperatorIds = Set<Int64>()
|
||||
@State private var reviewConditionsNavLinkActive = false
|
||||
@State private var sheetItem: ChooseServerOperatorsSheet? = nil
|
||||
@State private var notificationsModeNavLinkActive = false
|
||||
@State private var justOpened = true
|
||||
@@ -73,24 +72,33 @@ struct ChooseServerOperators: View {
|
||||
GeometryReader { g in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
if !onboarding {
|
||||
Text("Choose operators")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
let title = Text("Server operators")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
if onboarding {
|
||||
title.padding(.top, 25)
|
||||
} else {
|
||||
title
|
||||
}
|
||||
|
||||
infoText()
|
||||
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
Spacer()
|
||||
|
||||
ForEach(serverOperators) { srvOperator in
|
||||
operatorCheckView(srvOperator)
|
||||
}
|
||||
Text("You can configure servers via settings.")
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.horizontal, 32)
|
||||
VStack {
|
||||
Text("SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.").padding(.bottom, 8)
|
||||
Text("You can configure servers via settings.")
|
||||
}
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -117,24 +125,24 @@ struct ChooseServerOperators: View {
|
||||
.foregroundColor(.clear)
|
||||
}
|
||||
}
|
||||
.font(.callout)
|
||||
.padding(.top)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.frame(minHeight: 40)
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
if !onboarding && !reviewForOperators.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
reviewLaterButton()
|
||||
(
|
||||
Text("Conditions will be accepted for enabled operators after 30 days.")
|
||||
+ Text(" ")
|
||||
+ textSpace
|
||||
+ Text("You can configure operators in Network & servers settings.")
|
||||
)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.footnote)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.disabled(!canReviewLater)
|
||||
.padding(.bottom)
|
||||
}
|
||||
@@ -160,23 +168,18 @@ struct ChooseServerOperators: View {
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.padding(onboarding ? 25 : 16)
|
||||
}
|
||||
|
||||
private func infoText() -> some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "info.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.onTapGesture {
|
||||
sheetItem = .showInfo
|
||||
}
|
||||
|
||||
Text("Select network operators to use.")
|
||||
Button {
|
||||
sheetItem = .showInfo
|
||||
} label: {
|
||||
Label("How it helps privacy", systemImage: "info.circle")
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,23 +217,15 @@ struct ChooseServerOperators: View {
|
||||
}
|
||||
|
||||
private func reviewConditionsButton() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
reviewConditionsNavLinkActive = true
|
||||
} label: {
|
||||
Text("Review conditions")
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
|
||||
.disabled(selectedOperatorIds.isEmpty)
|
||||
|
||||
NavigationLink(isActive: $reviewConditionsNavLinkActive) {
|
||||
reviewConditionsDestinationView()
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
NavigationLink("Review conditions") {
|
||||
reviewConditionsView()
|
||||
.navigationTitle("Conditions of use")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
|
||||
.disabled(selectedOperatorIds.isEmpty)
|
||||
}
|
||||
|
||||
private func setOperatorsButton() -> some View {
|
||||
@@ -305,37 +300,27 @@ struct ChooseServerOperators: View {
|
||||
|
||||
private func notificationsModeDestinationView() -> some View {
|
||||
SetNotificationsMode()
|
||||
.navigationTitle("Push notifications")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
private func reviewConditionsDestinationView() -> some View {
|
||||
reviewConditionsView()
|
||||
.navigationTitle("Conditions of use")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
|
||||
@ViewBuilder private func reviewConditionsView() -> some View {
|
||||
let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted }
|
||||
let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted }
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
if !operatorsWithConditionsAccepted.isEmpty {
|
||||
Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
Text("Same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
Text("The same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
} else {
|
||||
Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
}
|
||||
ConditionsTextView()
|
||||
.frame(maxHeight: .infinity)
|
||||
acceptConditionsButton()
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(.horizontal, 25)
|
||||
}
|
||||
|
||||
private func acceptConditionsButton() -> some View {
|
||||
@@ -408,26 +393,56 @@ struct ChooseServerOperators: View {
|
||||
}
|
||||
}
|
||||
|
||||
let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")!
|
||||
|
||||
struct ChooseServerOperatorsInfoView: View {
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Network operators")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(.vertical)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Group {
|
||||
Text("When more than one network operator is enabled, the app will use the servers of different operators for each conversation.")
|
||||
Text("For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing.")
|
||||
NavigationView {
|
||||
List {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("The app protects your privacy by using different operators in each conversation.")
|
||||
Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.")
|
||||
Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.")
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.padding(.top)
|
||||
|
||||
Section {
|
||||
ForEach(ChatModel.shared.conditions.serverOperators) { op in
|
||||
operatorInfoNavLinkView(op)
|
||||
}
|
||||
.padding(.bottom)
|
||||
} header: {
|
||||
Text("About operators")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Server operators")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
}
|
||||
|
||||
private func operatorInfoNavLinkView(_ op: ServerOperator) -> some View {
|
||||
NavigationLink() {
|
||||
OperatorInfoView(serverOperator: op)
|
||||
.navigationBarTitle("Network operator")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(op.logo(colorScheme))
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 24, height: 24)
|
||||
Text(op.tradeName)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,49 +119,66 @@ struct CreateFirstProfile: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
Text("The profile is only shared with your contacts.")
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("Create your profile")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("The profile is only shared with your contacts.")
|
||||
.font(.callout)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity) // Ensures it takes up the full width
|
||||
.padding(.horizontal, 10)
|
||||
|
||||
HStack {
|
||||
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||
let validName = mkValidName(name)
|
||||
ZStack {
|
||||
ZStack(alignment: .trailing) {
|
||||
TextField("Enter your name…", text: $displayName)
|
||||
.focused($focusDisplayName)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color(uiColor: .tertiarySystemFill))
|
||||
)
|
||||
if name != validName {
|
||||
Button {
|
||||
showAlert(.invalidNameError(validName: validName))
|
||||
} label: {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.foregroundColor(.red)
|
||||
.padding(.horizontal, 10)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
||||
Image(systemName: "pencil").foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
TextField("Enter your name…", text: $displayName)
|
||||
.focused($focusDisplayName)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color(uiColor: .tertiarySystemFill))
|
||||
)
|
||||
}
|
||||
.padding(.top)
|
||||
|
||||
Spacer()
|
||||
|
||||
createProfileButton()
|
||||
.padding(.bottom)
|
||||
VStack(spacing: 10) {
|
||||
createProfileButton()
|
||||
if !focusDisplayName {
|
||||
onboardingButtonPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
focusDisplayName = true
|
||||
setLastVersionDefault()
|
||||
}
|
||||
.padding()
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 25)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
@@ -191,8 +208,6 @@ struct CreateFirstProfile: View {
|
||||
|
||||
private func nextStepDestinationView() -> some View {
|
||||
ChooseServerOperators(onboarding: true)
|
||||
.navigationTitle("Choose operators")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@@ -23,13 +23,10 @@ struct HowItWorks: View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Group {
|
||||
Text("Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*")
|
||||
Text("To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.")
|
||||
Text("You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.")
|
||||
Text("Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.")
|
||||
if onboarding {
|
||||
Text("Read more in our GitHub repository.")
|
||||
} else {
|
||||
Text("To protect your privacy, SimpleX uses separate IDs for each of your contacts.")
|
||||
Text("Only client devices store user profiles, contacts, groups, and messages.")
|
||||
Text("All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.")
|
||||
if !onboarding {
|
||||
Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).")
|
||||
}
|
||||
}
|
||||
@@ -40,12 +37,14 @@ struct HowItWorks: View {
|
||||
Spacer()
|
||||
|
||||
if onboarding {
|
||||
createFirstProfileButton()
|
||||
.padding(.bottom)
|
||||
VStack(spacing: 10) {
|
||||
createFirstProfileButton()
|
||||
onboardingButtonPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
.lineLimit(10)
|
||||
.padding()
|
||||
.padding(onboarding ? 25 : 16)
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
|
||||
@@ -24,14 +24,10 @@ struct OnboardingView: View {
|
||||
CreateSimpleXAddress()
|
||||
case .step3_ChooseServerOperators:
|
||||
ChooseServerOperators(onboarding: true)
|
||||
.navigationTitle("Choose operators")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.modifier(ThemedBackground())
|
||||
case .step4_SetNotificationsMode:
|
||||
SetNotificationsMode()
|
||||
.navigationTitle("Push notifications")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.modifier(ThemedBackground())
|
||||
case .onboardingComplete: EmptyView()
|
||||
@@ -40,6 +36,10 @@ struct OnboardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func onboardingButtonPlaceholder() -> some View {
|
||||
Spacer().frame(height: 40)
|
||||
}
|
||||
|
||||
enum OnboardingStage: String, Identifiable {
|
||||
case step1_SimpleXInfo
|
||||
case step2_CreateProfile // deprecated
|
||||
|
||||
@@ -13,41 +13,55 @@ struct SetNotificationsMode: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State private var notificationMode = NotificationsMode.instant
|
||||
@State private var showAlert: NotificationAlert?
|
||||
@State private var showInfo: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Send notifications:")
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("Push notifications")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(.top, 25)
|
||||
|
||||
infoText()
|
||||
|
||||
Spacer()
|
||||
|
||||
ForEach(NotificationsMode.values) { mode in
|
||||
NtfModeSelector(mode: mode, selection: $notificationMode)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
if let token = m.deviceToken {
|
||||
setNotificationsMode(token, notificationMode)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(title: "No device token!")
|
||||
}
|
||||
onboardingStageDefault.set(.onboardingComplete)
|
||||
m.onboardingStage = .onboardingComplete
|
||||
} label: {
|
||||
if case .off = notificationMode {
|
||||
Text("Use chat")
|
||||
} else {
|
||||
Text("Enable notifications")
|
||||
VStack(spacing: 10) {
|
||||
Button {
|
||||
if let token = m.deviceToken {
|
||||
setNotificationsMode(token, notificationMode)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(title: "No device token!")
|
||||
}
|
||||
onboardingStageDefault.set(.onboardingComplete)
|
||||
m.onboardingStage = .onboardingComplete
|
||||
} label: {
|
||||
if case .off = notificationMode {
|
||||
Text("Use chat")
|
||||
} else {
|
||||
Text("Enable notifications")
|
||||
}
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle())
|
||||
onboardingButtonPlaceholder()
|
||||
}
|
||||
.buttonStyle(OnboardingButtonStyle())
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding()
|
||||
.padding(25)
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.sheet(isPresented: $showInfo) {
|
||||
NotificationsInfoView()
|
||||
}
|
||||
}
|
||||
|
||||
private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) {
|
||||
@@ -73,6 +87,15 @@ struct SetNotificationsMode: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func infoText() -> some View {
|
||||
Button {
|
||||
showInfo = true
|
||||
} label: {
|
||||
Label("How it affects privacy", systemImage: "info.circle")
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NtfModeSelector: View {
|
||||
@@ -83,15 +106,25 @@ struct NtfModeSelector: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(mode.label)
|
||||
.font(.headline)
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: mode.icon)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: mode.icon == "bolt" ? 14 : 18, height: 18)
|
||||
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
|
||||
Text(ntfModeDescription(mode))
|
||||
.lineLimit(10)
|
||||
.font(.subheadline)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(mode.label)
|
||||
.font(.headline)
|
||||
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
|
||||
Text(ntfModeShortDescription(mode))
|
||||
.lineLimit(2)
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
|
||||
@@ -107,6 +140,37 @@ struct NtfModeSelector: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationsInfoView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Notifications privacy")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(.vertical)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Group {
|
||||
ForEach(NotificationsMode.values) { mode in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
(Text(Image(systemName: mode.icon)) + textSpace + Text(mode.label))
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
Text(ntfModeDescription(mode))
|
||||
.lineLimit(10)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationsModeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SetNotificationsMode()
|
||||
|
||||
@@ -19,51 +19,52 @@ struct SimpleXInfo: View {
|
||||
var body: some View {
|
||||
GeometryReader { g in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Image(colorScheme == .light ? "logo" : "logo-light")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: g.size.width * 0.67)
|
||||
.padding(.bottom, 8)
|
||||
.frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
Image(colorScheme == .light ? "logo" : "logo-light")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: g.size.width * 0.67)
|
||||
.padding(.bottom, 8)
|
||||
.padding(.leading, 4)
|
||||
.frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
|
||||
|
||||
Button {
|
||||
showHowItWorks = true
|
||||
} label: {
|
||||
Label("The future of messaging", systemImage: "info.circle")
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("The next generation of private messaging")
|
||||
.font(.title2)
|
||||
.padding(.bottom, 30)
|
||||
.padding(.horizontal, 40)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
infoRow("privacy", "Privacy redefined",
|
||||
"The 1st platform without any user identifiers – private by design.", width: 48)
|
||||
infoRow("shield", "Immune to spam and abuse",
|
||||
"People can connect to you only via the links you share.", width: 46)
|
||||
infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized",
|
||||
"Open-source protocol and code – anybody can run the servers.", width: 44)
|
||||
onboardingInfoRow("privacy", "Privacy redefined",
|
||||
"No user identifiers.", width: 48)
|
||||
onboardingInfoRow("shield", "Immune to spam",
|
||||
"You decide who can connect.", width: 46)
|
||||
onboardingInfoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized",
|
||||
"Anybody can host servers.", width: 46)
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
if onboarding {
|
||||
createFirstProfileButton()
|
||||
VStack(spacing: 10) {
|
||||
createFirstProfileButton()
|
||||
|
||||
Button {
|
||||
m.migrationState = .pasteOrScanLink
|
||||
} label: {
|
||||
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
|
||||
.font(.subheadline)
|
||||
Button {
|
||||
m.migrationState = .pasteOrScanLink
|
||||
} label: {
|
||||
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.frame(minHeight: 40)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
Button {
|
||||
showHowItWorks = true
|
||||
} label: {
|
||||
Label("How it works", systemImage: "info.circle")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
@@ -89,26 +90,29 @@ struct SimpleXInfo: View {
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 75)
|
||||
.padding(.bottom, 25)
|
||||
}
|
||||
|
||||
private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
|
||||
private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
Image(image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: width, height: 54)
|
||||
.frame(width: 54)
|
||||
.padding(.top, 4)
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, 10)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title).font(.headline)
|
||||
Text(text).frame(minHeight: 40, alignment: .top)
|
||||
.font(.callout)
|
||||
.lineLimit(3)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.padding(.bottom, 20)
|
||||
.padding(.trailing, 6)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
|
||||
private func createFirstProfileButton() -> some View {
|
||||
@@ -121,7 +125,7 @@ struct SimpleXInfo: View {
|
||||
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||
|
||||
NavigationLink(isActive: $createProfileNavLinkActive) {
|
||||
createProfileDestinationView()
|
||||
CreateFirstProfile()
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -129,15 +133,10 @@ struct SimpleXInfo: View {
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func createProfileDestinationView() -> some View {
|
||||
CreateFirstProfile()
|
||||
.navigationTitle("Create your profile")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground())
|
||||
}
|
||||
}
|
||||
|
||||
let textSpace = Text(verbatim: " ")
|
||||
|
||||
struct SimpleXInfo_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SimpleXInfo(onboarding: true)
|
||||
|
||||
@@ -520,14 +520,19 @@ private let versionDescriptions: [VersionDescription] = [
|
||||
]
|
||||
),
|
||||
VersionDescription(
|
||||
version: "v6.2 (beta.1)",
|
||||
post: URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html"),
|
||||
version: "v6.2",
|
||||
post: URL(string: "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html"),
|
||||
features: [
|
||||
.view(FeatureView(
|
||||
icon: nil,
|
||||
title: "Network decentralization",
|
||||
view: { NewOperatorsView() }
|
||||
)),
|
||||
.feature(Description(
|
||||
icon: "briefcase",
|
||||
title: "Business chats",
|
||||
description: "Privacy for your customers."
|
||||
)),
|
||||
.feature(Description(
|
||||
icon: "bolt",
|
||||
title: "More reliable notifications",
|
||||
|
||||
@@ -268,7 +268,7 @@ struct ConnectDesktopView: View {
|
||||
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
||||
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "")
|
||||
if (rc == nil) {
|
||||
t = t + Text(" ") + Text("(new)").italic()
|
||||
t = t + textSpace + Text("(new)").italic()
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -277,7 +277,7 @@ struct ConnectDesktopView: View {
|
||||
let v = session.ctrlAppInfo?.appVersionRange.maxVersion
|
||||
var t = Text("v\(v ?? "")")
|
||||
if v != session.appVersion {
|
||||
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
|
||||
t = t + textSpace + Text("(this device v\(session.appVersion))").italic()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ extension AppSettings {
|
||||
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
|
||||
if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) }
|
||||
if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) }
|
||||
if let val = chatBottomBar { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR) }
|
||||
}
|
||||
|
||||
public static var current: AppSettings {
|
||||
@@ -100,6 +101,7 @@ extension AppSettings {
|
||||
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
|
||||
c.uiThemes = themeOverridesDefault.get()
|
||||
c.oneHandUI = groupDefaults.bool(forKey: GROUP_DEFAULT_ONE_HAND_UI)
|
||||
c.chatBottomBar = groupDefaults.bool(forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR)
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ struct DeveloperView: View {
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
((developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option."))
|
||||
((developerTools ? Text("Show:") : Text("Hide:")) + textSpace + Text("Database IDs and Transport isolation option."))
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// ConditionsWebView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Stanislav Dmitrenko on 26.11.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
struct ConditionsWebView: UIViewRepresentable {
|
||||
@State var html: String
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State var pageLoaded = false
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let view = WKWebView()
|
||||
view.backgroundColor = .clear
|
||||
view.isOpaque = false
|
||||
view.navigationDelegate = context.coordinator
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
// just to make sure that even if updateUIView will not be called for any reason, the page
|
||||
// will be rendered anyway
|
||||
if !pageLoaded {
|
||||
loadPage(view)
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ view: WKWebView, context: Context) {
|
||||
loadPage(view)
|
||||
}
|
||||
|
||||
private func loadPage(_ webView: WKWebView) {
|
||||
let styles = """
|
||||
<style>
|
||||
body {
|
||||
color: \(theme.colors.onBackground.toHTMLHex());
|
||||
font-family: Helvetica;
|
||||
}
|
||||
a {
|
||||
color: \(theme.colors.primary.toHTMLHex());
|
||||
}
|
||||
code, pre {
|
||||
font-family: Menlo;
|
||||
background: \(theme.colors.secondary.opacity(theme.colors.isLight ? 0.2 : 0.3).toHTMLHex());
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
let head = "<head><meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no'>\(styles)</head>"
|
||||
webView.loadHTMLString(head + html, baseURL: nil)
|
||||
DispatchQueue.main.async {
|
||||
pageLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Cordinator {
|
||||
Cordinator()
|
||||
}
|
||||
|
||||
class Cordinator: NSObject, WKNavigationDelegate {
|
||||
func webView(_ webView: WKWebView,
|
||||
decidePolicyFor navigationAction: WKNavigationAction,
|
||||
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
|
||||
guard let url = navigationAction.request.url else { return decisionHandler(.allow) }
|
||||
|
||||
switch navigationAction.navigationType {
|
||||
case .linkActivated:
|
||||
decisionHandler(.cancel)
|
||||
if url.absoluteString.starts(with: "https://simplex.chat/contact#") {
|
||||
ChatModel.shared.appOpenUrl = url
|
||||
} else {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
default:
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,6 +269,7 @@ struct UsageConditionsView: View {
|
||||
}
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
case let .accepted(operators):
|
||||
Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
@@ -277,7 +278,7 @@ struct UsageConditionsView: View {
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.horizontal, 25)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import Ink
|
||||
|
||||
struct OperatorView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@@ -330,9 +331,12 @@ struct OperatorInfoView: View {
|
||||
Text(d)
|
||||
}
|
||||
}
|
||||
Link(serverOperator.info.website.absoluteString, destination: serverOperator.info.website)
|
||||
}
|
||||
Section {
|
||||
Link("\(serverOperator.info.website)", destination: URL(string: serverOperator.info.website)!)
|
||||
if let selfhost = serverOperator.info.selfhost {
|
||||
Section {
|
||||
Link(selfhost.text, destination: selfhost.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +346,7 @@ struct OperatorInfoView: View {
|
||||
struct ConditionsTextView: View {
|
||||
@State private var conditionsData: (UsageConditions, String?, UsageConditions?)?
|
||||
@State private var failedToLoad: Bool = false
|
||||
@State private var conditionsHTML: String? = nil
|
||||
|
||||
let defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
|
||||
|
||||
@@ -350,7 +355,18 @@ struct ConditionsTextView: View {
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.task {
|
||||
do {
|
||||
conditionsData = try await getUsageConditions()
|
||||
let conditions = try await getUsageConditions()
|
||||
let conditionsText = conditions.1
|
||||
let parentLink = "https://github.com/simplex-chat/simplex-chat/blob/\(conditions.0.conditionsCommit)"
|
||||
let preparedText: String?
|
||||
if let conditionsText {
|
||||
let prepared = prepareMarkdown(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines), parentLink)
|
||||
conditionsHTML = MarkdownParser().html(from: prepared)
|
||||
preparedText = prepared
|
||||
} else {
|
||||
preparedText = nil
|
||||
}
|
||||
conditionsData = (conditions.0, preparedText, conditions.2)
|
||||
} catch let error {
|
||||
logger.error("ConditionsTextView getUsageConditions error: \(responseError(error))")
|
||||
failedToLoad = true
|
||||
@@ -358,18 +374,16 @@ struct ConditionsTextView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Markdown & diff rendering
|
||||
// TODO Diff rendering
|
||||
@ViewBuilder private func viewBody() -> some View {
|
||||
if let (usageConditions, conditionsText, acceptedConditions) = conditionsData {
|
||||
if let conditionsText = conditionsText {
|
||||
ScrollView {
|
||||
Text(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
.padding()
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
)
|
||||
if let (usageConditions, _, _) = conditionsData {
|
||||
if let conditionsHTML {
|
||||
ConditionsWebView(html: conditionsHTML)
|
||||
.padding(6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
)
|
||||
} else {
|
||||
let conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/\(usageConditions.conditionsCommit)/PRIVACY.md"
|
||||
conditionsLinkView(conditionsLink)
|
||||
@@ -391,6 +405,16 @@ struct ConditionsTextView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func prepareMarkdown(_ text: String, _ parentLink: String) -> String {
|
||||
let localLinkRegex = try! NSRegularExpression(pattern: "\\[([^\\(]*)\\]\\(#.*\\)")
|
||||
let h1Regex = try! NSRegularExpression(pattern: "^# ")
|
||||
var text = localLinkRegex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "$1")
|
||||
text = h1Regex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "")
|
||||
return text
|
||||
.replacingOccurrences(of: "](/", with: "](\(parentLink)/")
|
||||
.replacingOccurrences(of: "](./", with: "](\(parentLink)/")
|
||||
}
|
||||
}
|
||||
|
||||
struct SingleOperatorUsageConditionsView: View {
|
||||
@@ -400,7 +424,6 @@ struct SingleOperatorUsageConditionsView: View {
|
||||
@Binding var userServers: [UserOperatorServers]
|
||||
@Binding var serverErrors: [UserServersError]
|
||||
var operatorIndex: Int
|
||||
@State private var usageConditionsNavLinkActive: Bool = false
|
||||
|
||||
var body: some View {
|
||||
viewBody()
|
||||
@@ -412,52 +435,45 @@ struct SingleOperatorUsageConditionsView: View {
|
||||
|
||||
// In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Group {
|
||||
viewHeader()
|
||||
ConditionsTextView()
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
viewHeader()
|
||||
ConditionsTextView()
|
||||
}
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal)
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
} else if !operatorsWithConditionsAccepted.isEmpty {
|
||||
|
||||
NavigationView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Group {
|
||||
viewHeader()
|
||||
Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
Text("Same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.")
|
||||
conditionsAppliedToOtherOperatorsText()
|
||||
usageConditionsNavLinkButton()
|
||||
viewHeader()
|
||||
Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||
Text("The same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.")
|
||||
conditionsAppliedToOtherOperatorsText()
|
||||
Spacer()
|
||||
|
||||
Spacer()
|
||||
|
||||
acceptConditionsButton()
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
acceptConditionsButton()
|
||||
usageConditionsNavLinkButton()
|
||||
}
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Group {
|
||||
viewHeader()
|
||||
Text("To use the servers of **\(userServers[operatorIndex].operator_.legalName_)**, accept conditions of use.")
|
||||
conditionsAppliedToOtherOperatorsText()
|
||||
ConditionsTextView()
|
||||
acceptConditionsButton()
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
viewHeader()
|
||||
Text("To use the servers of **\(userServers[operatorIndex].operator_.legalName_)**, accept conditions of use.")
|
||||
conditionsAppliedToOtherOperatorsText()
|
||||
ConditionsTextView()
|
||||
acceptConditionsButton()
|
||||
.padding(.bottom)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
}
|
||||
@@ -524,31 +540,16 @@ struct SingleOperatorUsageConditionsView: View {
|
||||
}
|
||||
|
||||
private func usageConditionsNavLinkButton() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
usageConditionsNavLinkActive = true
|
||||
} label: {
|
||||
Text("View conditions")
|
||||
}
|
||||
|
||||
NavigationLink(isActive: $usageConditionsNavLinkActive) {
|
||||
usageConditionsDestinationView()
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
NavigationLink("View conditions") {
|
||||
ConditionsTextView()
|
||||
.padding()
|
||||
.navigationTitle("Conditions of use")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
}
|
||||
|
||||
private func usageConditionsDestinationView() -> some View {
|
||||
ConditionsTextView()
|
||||
.padding()
|
||||
.padding(.bottom)
|
||||
.navigationTitle("Conditions of use")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.font(.callout)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -237,9 +237,17 @@ struct NotificationsView: View {
|
||||
|
||||
func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
|
||||
switch mode {
|
||||
case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)."
|
||||
case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have."
|
||||
case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from."
|
||||
case .off: return "**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app."
|
||||
case .periodic: return "**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata."
|
||||
case .instant: return "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from."
|
||||
}
|
||||
}
|
||||
|
||||
func ntfModeShortDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
|
||||
switch mode {
|
||||
case .off: return "Check messages when allowed."
|
||||
case .periodic: return "Check messages every 20 min."
|
||||
case .instant: return "E2E encrypted notifications."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
|
||||
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
||||
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
||||
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
|
||||
let DEFAULT_SHOULD_IMPORT_APP_SETTINGS = "shouldImportAppSettings"
|
||||
let DEFAULT_DEVELOPER_TOOLS = "developerTools"
|
||||
let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted"
|
||||
let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
|
||||
@@ -192,6 +193,8 @@ let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.sta
|
||||
let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE)
|
||||
let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE)
|
||||
|
||||
/// after importing new database, this flag will be set and unset only after importing app settings in `initializeChat` */
|
||||
let shouldImportAppSettingsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_IMPORT_APP_SETTINGS)
|
||||
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
|
||||
let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.DARK.themeName)
|
||||
let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] )
|
||||
|
||||
@@ -11,25 +11,50 @@ import SwiftUI
|
||||
struct UserAddressLearnMore: View {
|
||||
@State var showCreateAddressButton = false
|
||||
@State private var createAddressLinkActive = false
|
||||
|
||||
@State private var createOneTimeLinkActive = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
Text("You can share your address as a link or QR code - anybody can connect to you.")
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
(Text(Image(systemName: "envelope")).foregroundColor(.secondary) + textSpace + Text("Share address publicly").bold().font(.title2))
|
||||
Text("Share SimpleX address on social media.")
|
||||
Text("You won't lose your contacts if you later delete your address.")
|
||||
Text("When people request to connect, you can accept or reject it.")
|
||||
Text("Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).")
|
||||
|
||||
(Text(Image(systemName: "link.badge.plus")).foregroundColor(.secondary) + textSpace + Text("Share 1-time link with a friend").font(.title2).bold())
|
||||
.padding(.top)
|
||||
Text("1-time link can be used *with one contact only* - share in person or via any messenger.")
|
||||
Text("You can set connection name, to remember who the link was shared with.")
|
||||
|
||||
if !showCreateAddressButton {
|
||||
(Text(Image(systemName: "shield")).foregroundColor(.secondary) + textSpace + Text("Connection security").font(.title2).bold())
|
||||
.padding(.top)
|
||||
Text("SimpleX address and 1-time links are safe to share via any messenger.")
|
||||
Text("To protect against your link being replaced, you can compare contact security codes.")
|
||||
Text("Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).")
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
|
||||
Spacer()
|
||||
|
||||
if showCreateAddressButton {
|
||||
addressCreationButton()
|
||||
.padding()
|
||||
VStack {
|
||||
addressCreationButton()
|
||||
.padding(.bottom)
|
||||
|
||||
createOneTimeLinkButton()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
private func addressCreationButton() -> some View {
|
||||
@@ -52,6 +77,28 @@ struct UserAddressLearnMore: View {
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func createOneTimeLinkButton() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
createOneTimeLinkActive = true
|
||||
} label: {
|
||||
Text("Create 1-time link")
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
NavigationLink(isActive: $createOneTimeLinkActive) {
|
||||
NewChatView(selection: .invite)
|
||||
.navigationTitle("New chat")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserAddressLearnMore_Previews: PreviewProvider {
|
||||
|
||||
@@ -18,43 +18,28 @@ struct UserAddressView: View {
|
||||
@State var autoCreate = false
|
||||
@State private var aas = AutoAcceptState()
|
||||
@State private var savedAAS = AutoAcceptState()
|
||||
@State private var ignoreShareViaProfileChange = false
|
||||
@State private var showMailView = false
|
||||
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
|
||||
@State private var alert: UserAddressAlert?
|
||||
@State private var progressIndicator = false
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
|
||||
private enum UserAddressAlert: Identifiable {
|
||||
case deleteAddress
|
||||
case profileAddress(on: Bool)
|
||||
case shareOnCreate
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .deleteAddress: return "deleteAddress"
|
||||
case let .profileAddress(on): return "profileAddress \(on)"
|
||||
case .shareOnCreate: return "shareOnCreate"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
userAddressScrollView()
|
||||
.onDisappear {
|
||||
if savedAAS != aas {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
|
||||
message: NSLocalizedString("Settings were changed.", comment: "alert message"),
|
||||
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
|
||||
buttonAction: saveAAS,
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
}
|
||||
userAddressView()
|
||||
|
||||
if progressIndicator {
|
||||
ZStack {
|
||||
@@ -75,23 +60,6 @@ struct UserAddressView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@Namespace private var bottomID
|
||||
|
||||
private func userAddressScrollView() -> some View {
|
||||
ScrollViewReader { proxy in
|
||||
userAddressView()
|
||||
.onChange(of: keyboardVisible) { _ in
|
||||
if keyboardVisible {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
withAnimation {
|
||||
proxy.scrollTo(bottomID, anchor: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func userAddressView() -> some View {
|
||||
List {
|
||||
if let userAddress = chatModel.userAddress {
|
||||
@@ -100,14 +68,18 @@ struct UserAddressView: View {
|
||||
aas = AutoAcceptState(userAddress: userAddress)
|
||||
savedAAS = aas
|
||||
}
|
||||
.onChange(of: aas.enable) { _ in
|
||||
if !aas.enable { aas = AutoAcceptState() }
|
||||
}
|
||||
} else {
|
||||
Section {
|
||||
createAddressButton()
|
||||
} footer: {
|
||||
Text("Create an address to let people connect with you.")
|
||||
} header: {
|
||||
Text("For social media")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
createOneTimeLinkButton()
|
||||
} header: {
|
||||
Text("Or to share privately")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
@@ -123,8 +95,8 @@ struct UserAddressView: View {
|
||||
title: Text("Delete address?"),
|
||||
message:
|
||||
shareViaProfile
|
||||
? Text("All your contacts will remain connected. Profile update will be sent to your contacts.")
|
||||
: Text("All your contacts will remain connected."),
|
||||
? Text("All your contacts will remain connected. Profile update will be sent to your contacts.")
|
||||
: Text("All your contacts will remain connected."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
@@ -134,7 +106,6 @@ struct UserAddressView: View {
|
||||
chatModel.userAddress = nil
|
||||
chatModel.updateUser(u)
|
||||
if shareViaProfile {
|
||||
ignoreShareViaProfileChange = true
|
||||
shareViaProfile = false
|
||||
}
|
||||
}
|
||||
@@ -147,37 +118,12 @@ struct UserAddressView: View {
|
||||
}
|
||||
}, secondaryButton: .cancel()
|
||||
)
|
||||
case let .profileAddress(on):
|
||||
if on {
|
||||
return Alert(
|
||||
title: Text("Share address with contacts?"),
|
||||
message: Text("Profile update will be sent to your contacts."),
|
||||
primaryButton: .default(Text("Share")) {
|
||||
setProfileAddress(on)
|
||||
}, secondaryButton: .cancel() {
|
||||
ignoreShareViaProfileChange = true
|
||||
shareViaProfile = !on
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return Alert(
|
||||
title: Text("Stop sharing address?"),
|
||||
message: Text("Profile update will be sent to your contacts."),
|
||||
primaryButton: .default(Text("Stop sharing")) {
|
||||
setProfileAddress(on)
|
||||
}, secondaryButton: .cancel() {
|
||||
ignoreShareViaProfileChange = true
|
||||
shareViaProfile = !on
|
||||
}
|
||||
)
|
||||
}
|
||||
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."),
|
||||
primaryButton: .default(Text("Share")) {
|
||||
setProfileAddress(true)
|
||||
ignoreShareViaProfileChange = true
|
||||
setProfileAddress($progressIndicator, true)
|
||||
shareViaProfile = true
|
||||
}, secondaryButton: .cancel()
|
||||
)
|
||||
@@ -192,19 +138,39 @@ struct UserAddressView: View {
|
||||
SimpleXLinkQRCode(uri: userAddress.connReqContact)
|
||||
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
|
||||
shareQRCodeButton(userAddress)
|
||||
if MFMailComposeViewController.canSendMail() {
|
||||
shareViaEmailButton(userAddress)
|
||||
// if MFMailComposeViewController.canSendMail() {
|
||||
// shareViaEmailButton(userAddress)
|
||||
// }
|
||||
settingsRow("briefcase", color: theme.colors.secondary) {
|
||||
Toggle("Business address", isOn: $aas.business)
|
||||
.onChange(of: aas.business) { ba in
|
||||
if ba {
|
||||
aas.enable = true
|
||||
aas.incognito = false
|
||||
}
|
||||
saveAAS($aas, $savedAAS)
|
||||
}
|
||||
}
|
||||
shareWithContactsButton()
|
||||
autoAcceptToggle()
|
||||
learnMoreButton()
|
||||
addressSettingsButton(userAddress)
|
||||
} header: {
|
||||
Text("Address")
|
||||
Text("For social media")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
if aas.business {
|
||||
Text("Add your team members to the conversations.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
createOneTimeLinkButton()
|
||||
} header: {
|
||||
Text("Or to share privately")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
if aas.enable {
|
||||
autoAcceptSection()
|
||||
Section {
|
||||
learnMoreButton()
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -213,7 +179,6 @@ struct UserAddressView: View {
|
||||
Text("Your contacts will remain connected.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.id(bottomID)
|
||||
}
|
||||
|
||||
private func createAddressButton() -> some View {
|
||||
@@ -223,7 +188,7 @@ struct UserAddressView: View {
|
||||
Label("Create SimpleX address", systemImage: "qrcode")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func createAddress() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
@@ -243,6 +208,18 @@ struct UserAddressView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func createOneTimeLinkButton() -> some View {
|
||||
NavigationLink {
|
||||
NewChatView(selection: .invite)
|
||||
.navigationTitle("New chat")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Label("Create 1-time link", systemImage: "link.badge.plus")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteAddressButton() -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .deleteAddress
|
||||
@@ -292,24 +269,140 @@ struct UserAddressView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func autoAcceptToggle() -> some View {
|
||||
settingsRow("checkmark", color: theme.colors.secondary) {
|
||||
Toggle("Auto-accept", isOn: $aas.enable)
|
||||
.onChange(of: aas.enable) { _ in
|
||||
saveAAS()
|
||||
}
|
||||
private func addressSettingsButton(_ userAddress: UserContactLink) -> some View {
|
||||
NavigationLink {
|
||||
UserAddressSettingsView(shareViaProfile: $shareViaProfile)
|
||||
.navigationTitle("Address settings")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text("Address settings")
|
||||
}
|
||||
}
|
||||
|
||||
private func learnMoreButton() -> some View {
|
||||
NavigationLink {
|
||||
UserAddressLearnMore()
|
||||
.navigationTitle("SimpleX address")
|
||||
.navigationTitle("Address or 1-time link?")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
settingsRow("info.circle", color: theme.colors.secondary) {
|
||||
Text("About SimpleX address")
|
||||
Text("SimpleX address or 1-time link?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AutoAcceptState: Equatable {
|
||||
var enable = false
|
||||
var incognito = false
|
||||
var business = false
|
||||
var welcomeText = ""
|
||||
|
||||
init(enable: Bool = false, incognito: Bool = false, business: Bool = false, welcomeText: String = "") {
|
||||
self.enable = enable
|
||||
self.incognito = incognito
|
||||
self.business = business
|
||||
self.welcomeText = welcomeText
|
||||
}
|
||||
|
||||
init(userAddress: UserContactLink) {
|
||||
if let aa = userAddress.autoAccept {
|
||||
enable = true
|
||||
incognito = aa.acceptIncognito
|
||||
business = aa.businessAddress
|
||||
if let msg = aa.autoReply {
|
||||
welcomeText = msg.text
|
||||
} else {
|
||||
welcomeText = ""
|
||||
}
|
||||
} else {
|
||||
enable = false
|
||||
incognito = false
|
||||
business = false
|
||||
welcomeText = ""
|
||||
}
|
||||
}
|
||||
|
||||
var autoAccept: AutoAccept? {
|
||||
if enable {
|
||||
var autoReply: MsgContent? = nil
|
||||
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if s != "" { autoReply = .text(s) }
|
||||
return AutoAccept(businessAddress: business, acceptIncognito: incognito, autoReply: autoReply)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func setProfileAddress(_ progressIndicator: Binding<Bool>, _ on: Bool) {
|
||||
progressIndicator.wrappedValue = true
|
||||
Task {
|
||||
do {
|
||||
if let u = try await apiSetProfileAddress(on: on) {
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.updateUser(u)
|
||||
}
|
||||
}
|
||||
await MainActor.run { progressIndicator.wrappedValue = false }
|
||||
} catch let error {
|
||||
logger.error("apiSetProfileAddress: \(responseError(error))")
|
||||
await MainActor.run { progressIndicator.wrappedValue = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserAddressSettingsView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var shareViaProfile: Bool
|
||||
@State private var aas = AutoAcceptState()
|
||||
@State private var savedAAS = AutoAcceptState()
|
||||
@State private var ignoreShareViaProfileChange = false
|
||||
@State private var progressIndicator = false
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let userAddress = ChatModel.shared.userAddress {
|
||||
userAddressSettingsView()
|
||||
.onAppear {
|
||||
aas = AutoAcceptState(userAddress: userAddress)
|
||||
savedAAS = aas
|
||||
}
|
||||
.onChange(of: aas.enable) { aasEnabled in
|
||||
if !aasEnabled { aas = AutoAcceptState() }
|
||||
}
|
||||
.onDisappear {
|
||||
if savedAAS != aas {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
|
||||
message: NSLocalizedString("Settings were changed.", comment: "alert message"),
|
||||
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
|
||||
buttonAction: { saveAAS($aas, $savedAAS) },
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(String("Error opening address settings"))
|
||||
}
|
||||
if progressIndicator {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func userAddressSettingsView() -> some View {
|
||||
List {
|
||||
Section {
|
||||
shareWithContactsButton()
|
||||
autoAcceptToggle().disabled(aas.business)
|
||||
}
|
||||
|
||||
if aas.enable {
|
||||
autoAcceptSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,70 +414,70 @@ struct UserAddressView: View {
|
||||
if ignoreShareViaProfileChange {
|
||||
ignoreShareViaProfileChange = false
|
||||
} else {
|
||||
alert = .profileAddress(on: on)
|
||||
if on {
|
||||
showAlert(
|
||||
NSLocalizedString("Share address with contacts?", comment: "alert title"),
|
||||
message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"),
|
||||
actions: {[
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Cancel", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
ignoreShareViaProfileChange = true
|
||||
shareViaProfile = !on
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Share", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
setProfileAddress($progressIndicator, on)
|
||||
}
|
||||
)
|
||||
]}
|
||||
)
|
||||
} else {
|
||||
showAlert(
|
||||
NSLocalizedString("Stop sharing address?", comment: "alert title"),
|
||||
message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"),
|
||||
actions: {[
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Cancel", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
ignoreShareViaProfileChange = true
|
||||
shareViaProfile = !on
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Stop sharing", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
setProfileAddress($progressIndicator, on)
|
||||
}
|
||||
)
|
||||
]}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setProfileAddress(_ on: Bool) {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
if let u = try await apiSetProfileAddress(on: on) {
|
||||
DispatchQueue.main.async {
|
||||
chatModel.updateUser(u)
|
||||
}
|
||||
private func autoAcceptToggle() -> some View {
|
||||
settingsRow("checkmark", color: theme.colors.secondary) {
|
||||
Toggle("Auto-accept", isOn: $aas.enable)
|
||||
.onChange(of: aas.enable) { _ in
|
||||
saveAAS($aas, $savedAAS)
|
||||
}
|
||||
await MainActor.run { progressIndicator = false }
|
||||
} catch let error {
|
||||
logger.error("UserAddressView apiSetProfileAddress: \(responseError(error))")
|
||||
await MainActor.run { progressIndicator = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AutoAcceptState: Equatable {
|
||||
var enable = false
|
||||
var incognito = false
|
||||
var welcomeText = ""
|
||||
|
||||
init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") {
|
||||
self.enable = enable
|
||||
self.incognito = incognito
|
||||
self.welcomeText = welcomeText
|
||||
}
|
||||
|
||||
init(userAddress: UserContactLink) {
|
||||
if let aa = userAddress.autoAccept {
|
||||
enable = true
|
||||
incognito = aa.acceptIncognito
|
||||
if let msg = aa.autoReply {
|
||||
welcomeText = msg.text
|
||||
} else {
|
||||
welcomeText = ""
|
||||
}
|
||||
} else {
|
||||
enable = false
|
||||
incognito = false
|
||||
welcomeText = ""
|
||||
}
|
||||
}
|
||||
|
||||
var autoAccept: AutoAccept? {
|
||||
if enable {
|
||||
var autoReply: MsgContent? = nil
|
||||
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if s != "" { autoReply = .text(s) }
|
||||
return AutoAccept(acceptIncognito: incognito, autoReply: autoReply)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func autoAcceptSection() -> some View {
|
||||
private func autoAcceptSection() -> some View {
|
||||
Section {
|
||||
acceptIncognitoToggle()
|
||||
if !aas.business {
|
||||
acceptIncognitoToggle()
|
||||
}
|
||||
welcomeMessageEditor()
|
||||
saveAASButton()
|
||||
.disabled(aas == savedAAS)
|
||||
@@ -424,22 +517,24 @@ struct UserAddressView: View {
|
||||
private func saveAASButton() -> some View {
|
||||
Button {
|
||||
keyboardVisible = false
|
||||
saveAAS()
|
||||
saveAAS($aas, $savedAAS)
|
||||
} label: {
|
||||
Text("Save")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAAS() {
|
||||
Task {
|
||||
do {
|
||||
if let address = try await userAddressAutoAccept(aas.autoAccept) {
|
||||
chatModel.userAddress = address
|
||||
savedAAS = aas
|
||||
private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAcceptState>) {
|
||||
Task {
|
||||
do {
|
||||
if let address = try await userAddressAutoAccept(aas.wrappedValue.autoAccept) {
|
||||
await MainActor.run {
|
||||
ChatModel.shared.userAddress = address
|
||||
savedAAS.wrappedValue = aas.wrappedValue
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("userAddressAutoAccept error: \(responseError(error))")
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("userAddressAutoAccept error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ struct UserProfilesView: View {
|
||||
action()
|
||||
} else {
|
||||
authenticate(
|
||||
reason: NSLocalizedString("Change user profiles", comment: "authentication reason")
|
||||
reason: NSLocalizedString("Change chat profiles", comment: "authentication reason")
|
||||
) { laResult in
|
||||
switch laResult {
|
||||
case .success, .unavailable:
|
||||
|
||||
@@ -187,23 +187,18 @@
|
||||
<target state="needs-translation">)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<target state="translated">** إضافة جهة اتصال جديدة **: لإنشاء رمز QR لمرة واحدة أو رابط جهة الاتصال الخاصة بكم.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<target state="translated">** أنشئ رابطًا / رمز QR ** لتستخدمه جهة الاتصال الخاصة بك.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<target state="translated">** المزيد من الخصوصية **: تحققوا من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديكم.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<target state="translated">** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامكم للتطبيق).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -217,8 +212,8 @@
|
||||
<target state="translated">** يرجى ملاحظة **: لن تتمكنوا من استعادة أو تغيير عبارة المرور إذا فقدتموها.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<target state="translated">** موصى به **: يتم إرسال رمز الجهاز والإشعارات إلى خادم إشعارات SimpleX Chat ، ولكن ليس محتوى الرسالة أو حجمها أو مصدرها.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1048,8 +1043,8 @@
|
||||
<source>Direct messages</source>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1064,8 +1059,8 @@
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -1428,16 +1423,16 @@
|
||||
<source>Group members can irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||
<source>Members can send direct messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||
<source>Members can send voice messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -1528,8 +1523,8 @@
|
||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||
<source>Immune to spam</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import" xml:space="preserve">
|
||||
@@ -1625,8 +1620,8 @@
|
||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
@@ -1926,8 +1921,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -1978,8 +1973,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Open user profiles</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -2010,8 +2005,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -2590,8 +2585,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||
@@ -2622,8 +2617,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>The microphone does not work when the app is in the background.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -2686,8 +2681,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>To prevent the call interruption, enable Do Not Disturb mode.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -2860,8 +2855,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -2972,10 +2967,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>You can use markdown to format messages:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -3752,8 +3743,8 @@ SimpleX servers cannot see your profile.</source>
|
||||
<source>%u messages skipped.</source>
|
||||
<target state="translated">%u تم تخطي الرسائل.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve" approved="no">
|
||||
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
|
||||
<trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no">
|
||||
<source>**Create 1-time link**: to create and share a new invitation link.</source>
|
||||
<target state="translated">**إضافة جهة اتصال**: لإنشاء رابط دعوة جديد، أو الاتصال عبر الرابط الذي تلقيتوهم.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
@@ -193,20 +193,16 @@
|
||||
<source>)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||
@@ -217,8 +213,8 @@
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||
@@ -1251,8 +1247,8 @@
|
||||
<source>Direct messages</source>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1271,8 +1267,8 @@
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at" xml:space="preserve">
|
||||
@@ -1751,24 +1747,24 @@
|
||||
<source>Group links</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can add message reactions." xml:space="preserve">
|
||||
<source>Group members can add message reactions.</source>
|
||||
<trans-unit id="Members can add message reactions." xml:space="preserve">
|
||||
<source>Members can add message reactions.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
|
||||
<source>Group members can irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||
<source>Members can send direct messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||
<source>Members can send voice messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -1899,8 +1895,8 @@
|
||||
<source>Immediately</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||
<source>Immune to spam</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import" xml:space="preserve">
|
||||
@@ -2020,8 +2016,8 @@
|
||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
@@ -2207,8 +2203,8 @@
|
||||
<source>Message reactions are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reactions are prohibited in this group." xml:space="preserve">
|
||||
<source>Message reactions are prohibited in this group.</source>
|
||||
<trans-unit id="Message reactions are prohibited." xml:space="preserve">
|
||||
<source>Message reactions are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message text" xml:space="preserve">
|
||||
@@ -2239,8 +2235,8 @@
|
||||
<source>Migration is completed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrations: %@" xml:space="preserve">
|
||||
<source>Migrations: %@</source>
|
||||
<trans-unit id="Migrations:" xml:space="preserve">
|
||||
<source>Migrations:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderate" xml:space="preserve">
|
||||
@@ -2409,8 +2405,8 @@
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -2477,8 +2473,8 @@
|
||||
<source>Open user profiles</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening database…" xml:space="preserve">
|
||||
@@ -2537,8 +2533,8 @@
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -3373,8 +3369,8 @@
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
||||
@@ -3418,8 +3414,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>The message will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -3490,8 +3486,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>To make a new connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -3724,8 +3720,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -3876,10 +3872,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>You can't send messages!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
@@ -186,20 +186,16 @@ Available in v5.1</source>
|
||||
<source>)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||
@@ -210,8 +206,8 @@ Available in v5.1</source>
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||
@@ -1128,8 +1124,8 @@ Available in v5.1</source>
|
||||
<source>Direct messages</source>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1144,8 +1140,8 @@ Available in v5.1</source>
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -1580,16 +1576,16 @@ Available in v5.1</source>
|
||||
<source>Group members can irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||
<source>Members can send direct messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||
<source>Members can send voice messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -1708,8 +1704,8 @@ Available in v5.1</source>
|
||||
<source>Immediately</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||
<source>Immune to spam</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import" xml:space="preserve">
|
||||
@@ -1821,8 +1817,8 @@ Available in v5.1</source>
|
||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
@@ -2016,8 +2012,8 @@ Available in v5.1</source>
|
||||
<source>Migration is completed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrations: %@" xml:space="preserve">
|
||||
<source>Migrations: %@</source>
|
||||
<trans-unit id="Migrations:" xml:space="preserve">
|
||||
<source>Migrations:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderate" xml:space="preserve">
|
||||
@@ -2174,8 +2170,8 @@ Available in v5.1</source>
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -2234,8 +2230,8 @@ Available in v5.1</source>
|
||||
<source>Open user profiles</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -2290,8 +2286,8 @@ Available in v5.1</source>
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -2994,8 +2990,8 @@ Available in v5.1</source>
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
||||
@@ -3039,8 +3035,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>The message will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -3111,8 +3107,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>To make a new connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -3337,8 +3333,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -3478,10 +3474,6 @@ SimpleX Lock must be enabled.</source>
|
||||
<source>You can't send messages!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
@@ -217,23 +217,18 @@ Available in v5.1</source>
|
||||
<target state="translated">)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<target state="translated">**הוסיפו איש קשר חדש**: ליצירת קוד QR או קישור חד־פעמיים עבור איש הקשר שלכם.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<target state="translated">**צור קישור / קוד QR** לשימוש איש הקשר שלך.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<target state="translated">**יותר פרטי**: בדוק הודעות חדשות כל 20 דקות. אסימון המכשיר משותף עם שרת SimpleX Chat, אך לא כמה אנשי קשר או הודעות יש לך.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<target state="translated">**הכי פרטי**: אל תשתמש בשרת ההתראות של SimpleX Chat, בדוק הודעות מעת לעת ברקע (תלוי בתדירות השימוש באפליקציה).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -247,8 +242,8 @@ Available in v5.1</source>
|
||||
<target state="translated">**שימו לב**: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<target state="translated">**מומלץ**: אסימון מכשיר והתראות נשלחים לשרת ההתראות של SimpleX Chat, אך לא תוכן ההודעה, גודלה או ממי היא.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1391,8 +1386,8 @@ Available in v5.1</source>
|
||||
<target state="translated">הודעות ישירות</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<target state="translated">הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1411,8 +1406,8 @@ Available in v5.1</source>
|
||||
<target state="translated">הודעות נעלמות אסורות בצ׳אט זה.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<target state="translated">הודעות נעלמות אסורות בקבוצה זו.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1956,18 +1951,18 @@ Available in v5.1</source>
|
||||
<target state="translated">חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve" approved="no">
|
||||
<source>Members can send direct messages.</source>
|
||||
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות ישירות.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve" approved="no">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות נעלמות.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve" approved="no">
|
||||
<source>Members can send voice messages.</source>
|
||||
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות קוליות.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -2115,8 +2110,8 @@ Available in v5.1</source>
|
||||
<target state="translated">מיד</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve" approved="no">
|
||||
<source>Immune to spam</source>
|
||||
<target state="translated">חסין מפני ספאם ושימוש לרעה</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -2257,8 +2252,8 @@ Available in v5.1</source>
|
||||
<target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve" approved="no">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -2502,9 +2497,9 @@ Available in v5.1</source>
|
||||
<target state="translated">ההעברה הושלמה</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrations: %@" xml:space="preserve" approved="no">
|
||||
<source>Migrations: %@</source>
|
||||
<target state="translated">העברות: %@</target>
|
||||
<trans-unit id="Migrations:" xml:space="preserve" approved="no">
|
||||
<source>Migrations:</source>
|
||||
<target state="translated">העברות:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderate" xml:space="preserve" approved="no">
|
||||
@@ -2701,8 +2696,8 @@ Available in v5.1</source>
|
||||
<target state="translated">לא ייעשה שימוש במארחי Onion.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -2761,8 +2756,8 @@ Available in v5.1</source>
|
||||
<source>Open user profiles</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -2817,8 +2812,8 @@ Available in v5.1</source>
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -3521,8 +3516,8 @@ Available in v5.1</source>
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
||||
@@ -3566,8 +3561,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>The message will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -3638,8 +3633,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>To make a new connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -3864,8 +3859,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -4005,10 +4000,6 @@ SimpleX Lock must be enabled.</source>
|
||||
<source>You can't send messages!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4967,8 +4958,8 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target state="translated">נמחק</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Files and media are prohibited in this group.</source>
|
||||
<trans-unit id="Files and media are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Files and media are prohibited.</source>
|
||||
<target state="translated">קבצים ומדיה אסורים בקבוצה זו.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -5027,13 +5018,13 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target state="translated">הזמן חברים</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can add message reactions." xml:space="preserve" approved="no">
|
||||
<source>Group members can add message reactions.</source>
|
||||
<trans-unit id="Members can add message reactions." xml:space="preserve" approved="no">
|
||||
<source>Members can add message reactions.</source>
|
||||
<target state="translated">חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send files and media." xml:space="preserve" approved="no">
|
||||
<source>Group members can send files and media.</source>
|
||||
<trans-unit id="Members can send files and media." xml:space="preserve" approved="no">
|
||||
<source>Members can send files and media.</source>
|
||||
<target state="translated">חברי הקבוצה יכולים לשלוח קבצים ומדיה.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -5231,8 +5222,8 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target state="translated">תגובות אמוג׳י להודעות אסורות בצ׳אט זה.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Message reactions are prohibited in this group.</source>
|
||||
<trans-unit id="Message reactions are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Message reactions are prohibited.</source>
|
||||
<target state="translated">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -181,23 +181,18 @@
|
||||
<source>)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<target state="needs-translation">**Dodajte novi kontakt**: da biste stvorili svoj jednokratni QR kôd ili vezu za svoj kontakt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<target state="needs-translation">**Stvorite vezu / QR kôd** za vaš kontakt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<target state="needs-translation">**Privatnije**: provjeravajte nove poruke svakih 20 minuta. Token uređaja dijeli se s SimpleX Chat poslužiteljem, ali ne i s brojem kontakata ili poruka koje imate.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<target state="needs-translation">**Najprivatniji**: nemojte koristiti SimpleX Chat poslužitelj obavijesti, povremeno provjeravajte poruke u pozadini (ovisi o tome koliko često koristite aplikaciju).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -211,8 +206,8 @@
|
||||
<target state="needs-translation">**Imajte na umu**: NEĆETE moći oporaviti ili promijeniti pristupni izraz ako ga izgubite.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<target state="needs-translation">**Preporučeno**: token uređaja i obavijesti šalju se na poslužitelj obavijesti SimpleX Chata, ali ne i sadržaj poruke, veličinu ili od koga je.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1039,8 +1034,8 @@
|
||||
<source>Direct messages</source>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1055,8 +1050,8 @@
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -1419,16 +1414,16 @@
|
||||
<source>Group members can irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||
<source>Members can send direct messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||
<source>Members can send voice messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -1519,8 +1514,8 @@
|
||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||
<source>Immune to spam</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import" xml:space="preserve">
|
||||
@@ -1616,8 +1611,8 @@
|
||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
@@ -1917,8 +1912,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -1965,8 +1960,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Open chat console</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -1997,8 +1992,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -2577,8 +2572,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||
@@ -2609,8 +2604,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>The microphone does not work when the app is in the background.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -2673,8 +2668,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>To prevent the call interruption, enable Do Not Disturb mode.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -2847,8 +2842,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -2959,10 +2954,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>You can use markdown to format messages:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
@@ -152,20 +152,16 @@
|
||||
<source>)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||
@@ -176,8 +172,8 @@
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||
@@ -1013,8 +1009,8 @@
|
||||
<source>Direct messages</source>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1029,8 +1025,8 @@
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -1421,16 +1417,16 @@
|
||||
<source>Group members can irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||
<source>Members can send direct messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||
<source>Members can send voice messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -1537,8 +1533,8 @@
|
||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||
<source>Immune to spam</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import" xml:space="preserve">
|
||||
@@ -1642,8 +1638,8 @@
|
||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
@@ -1961,8 +1957,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -2013,8 +2009,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Open user profiles</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -2049,8 +2045,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -2670,8 +2666,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||
@@ -2706,8 +2702,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>The message will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -2774,8 +2770,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>To make a new connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -2968,8 +2964,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -3093,10 +3089,6 @@ SimpleX Lock must be enabled.</source>
|
||||
<source>You can't send messages!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
||||
@@ -162,20 +162,16 @@
|
||||
<target state="translated">)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||
@@ -187,8 +183,8 @@
|
||||
<target state="translated">**Turėkite omenyje**: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||
@@ -1033,8 +1029,8 @@
|
||||
<source>Direct messages</source>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1049,8 +1045,8 @@
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -1417,16 +1413,16 @@
|
||||
<source>Group members can irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||
<source>Members can send direct messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||
<source>Members can send voice messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -1513,8 +1509,8 @@
|
||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||
<source>Immune to spam</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import" xml:space="preserve">
|
||||
@@ -1614,8 +1610,8 @@
|
||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
@@ -1919,8 +1915,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -1971,8 +1967,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Open user profiles</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -2003,8 +1999,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -2591,8 +2587,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||
@@ -2627,8 +2623,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>The message will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -2687,8 +2683,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>To make a new connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -2873,8 +2869,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -2993,10 +2989,6 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>You can't send messages!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
@@ -187,23 +187,18 @@
|
||||
<target state="translated">)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<target state="translated">**Adicionar novo contato**: para criar seu QR Code ou link único para seu contato.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<target state="translated">**Crie um link / QR code** para seu contato usar.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<target state="translated">**Mais privado**: verifique as novas mensagens a cada 20 minutos. O token do dispositivo é compartilhado com o servidor do SimpleX Chat, mas não quantos contatos ou mensagens você tem.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<target state="translated">**Mais privado**: não use o servidor de notificações do SimpleX Chat, verifique as mensagens periodicamente em segundo plano (depende da frequência com que você usa o aplicativo).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -217,8 +212,8 @@
|
||||
<target state="translated">**Observação**: NÃO será possível recuperar ou alterar a frase secreta se você a perder.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<target state="translated">**Recomendado**: o token do dispositivo e as notificações são enviados para o servidor de notificações do SimpleX Chat, mas não o conteúdo, o tamanho ou o remetente da mensagem.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1209,8 +1204,8 @@
|
||||
<target state="translated">Mensagens diretas</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<target state="translated">Mensagens diretas entre membros são proibidas neste grupo.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1229,8 +1224,8 @@
|
||||
<target state="translated">Mensagens temporárias são proibidas nesse chat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<target state="translated">Mensagens que temporárias são proibidas neste grupo.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1643,18 +1638,18 @@
|
||||
<target state="translated">Os membros do grupo podem excluir mensagens enviadas de forma irreversível.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve" approved="no">
|
||||
<source>Members can send direct messages.</source>
|
||||
<target state="translated">Os membros do grupo podem enviar DMs.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve" approved="no">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<target state="translated">Os membros do grupo podem enviar mensagens que desaparecem.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve" approved="no">
|
||||
<source>Members can send voice messages.</source>
|
||||
<target state="translated">Os membros do grupo podem enviar mensagens de voz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1761,8 +1756,8 @@
|
||||
<target state="translated">A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve" approved="no">
|
||||
<source>Immune to spam</source>
|
||||
<target state="translated">Imune a spam e abuso</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1878,8 +1873,8 @@
|
||||
<target state="translated">A exclusão irreversível de mensagens é proibida neste chat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve" approved="no">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<target state="translated">A exclusão irreversível de mensagens é proibida neste grupo.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -2209,8 +2204,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target state="translated">Hosts Onion não serão usados.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve" approved="no">
|
||||
@@ -2267,8 +2262,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target state="translated">Abrir console de chat</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve" approved="no">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve" approved="no">
|
||||
<source>Anybody can host servers.</source>
|
||||
<target state="translated">Protocolo de código aberto – qualquer um pode executar os servidores.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -2306,8 +2301,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target state="translated">Cole o link que você recebeu na caixa abaixo para conectar com o seu contato.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve" approved="no">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve" approved="no">
|
||||
<source>You decide who can connect.</source>
|
||||
<target state="translated">Pessoas podem se conectar com você somente via links compartilhados.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -2961,8 +2956,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>Thank you for installing SimpleX Chat!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve" approved="no">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve" approved="no">
|
||||
<source>No user identifiers.</source>
|
||||
<target state="translated">A 1ª plataforma sem nenhum identificador de usuário – privada por design.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -2998,8 +2993,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>The microphone does not work when the app is in the background.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve" approved="no">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve" approved="no">
|
||||
<source>The future of messaging</source>
|
||||
<target state="translated">A próxima geração de mensageiros privados</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -3071,8 +3066,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<source>To prevent the call interruption, enable Do Not Disturb mode.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve" approved="no">
|
||||
@@ -3274,8 +3269,8 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif
|
||||
<target state="translated">Mensagens de voz são proibidas neste chat.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve" approved="no">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<target state="translated">Mensagens de voz são proibidas neste grupo.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -3402,10 +3397,6 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif
|
||||
<target state="translated">Você pode usar markdown para formatar mensagens:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve" approved="no">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<target state="translated">Você não pôde ser verificado; por favor, tente novamente.</target>
|
||||
@@ -5434,8 +5425,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
|
||||
<source>Advanced settings</source>
|
||||
<target state="translated">Configurações avançadas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All data is private to your device." xml:space="preserve" approved="no">
|
||||
<source>All data is private to your device.</source>
|
||||
<trans-unit id="All data is kept private on your device." xml:space="preserve" approved="no">
|
||||
<source>All data is kept private on your device.</source>
|
||||
<target state="translated">Toda informação é privada em seu dispositivo.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no">
|
||||
@@ -5482,8 +5473,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
|
||||
<source>(this device v%@)</source>
|
||||
<target state="translated">este dispositivo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve" approved="no">
|
||||
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
|
||||
<trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no">
|
||||
<source>**Create 1-time link**: to create and share a new invitation link.</source>
|
||||
<target state="translated">**Adicionar contato**: criar um novo link de convite ou conectar via um link que você recebeu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
|
||||
@@ -5570,6 +5561,90 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
|
||||
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
|
||||
<target state="translated">Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat migrated!" xml:space="preserve" approved="no">
|
||||
<source>Chat migrated!</source>
|
||||
<target state="translated">Conversa migrada!</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Auto-accept settings" xml:space="preserve" approved="no">
|
||||
<source>Auto-accept settings</source>
|
||||
<target state="translated">Aceitar automaticamente configurações</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target state="translated">O aplicativo criptografa novos arquivos locais (exceto videos).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="App session" xml:space="preserve" approved="no">
|
||||
<source>App session</source>
|
||||
<target state="translated">Sessão do aplicativo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Acknowledged" xml:space="preserve" approved="no">
|
||||
<source>Acknowledged</source>
|
||||
<target state="translated">Reconhecido</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Acknowledgement errors" xml:space="preserve" approved="no">
|
||||
<source>Acknowledgement errors</source>
|
||||
<target state="translated">Erros conhecidos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat list" xml:space="preserve" approved="no">
|
||||
<source>Chat list</source>
|
||||
<target state="translated">Lista de conversas</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat database exported" xml:space="preserve" approved="no">
|
||||
<source>Chat database exported</source>
|
||||
<target state="translated">Banco de dados da conversa exportado</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat preferences were changed." xml:space="preserve" approved="no">
|
||||
<source>Chat preferences were changed.</source>
|
||||
<target state="translated">As preferências de bate-papo foram alteradas.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat theme" xml:space="preserve" approved="no">
|
||||
<source>Chat theme</source>
|
||||
<target state="translated">Tema da conversa</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Better calls" xml:space="preserve" approved="no">
|
||||
<source>Better calls</source>
|
||||
<target state="translated">Chamadas melhores</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Better user experience" xml:space="preserve" approved="no">
|
||||
<source>Better user experience</source>
|
||||
<target state="translated">Melhor experiência do usuário</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow downgrade" xml:space="preserve" approved="no">
|
||||
<source>Allow downgrade</source>
|
||||
<target state="translated">Permitir redução</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Additional secondary" xml:space="preserve" approved="no">
|
||||
<source>Additional secondary</source>
|
||||
<target state="translated">Secundária adicional</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="App data migration" xml:space="preserve" approved="no">
|
||||
<source>App data migration</source>
|
||||
<target state="translated">Migração de dados do aplicativo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive and upload" xml:space="preserve" approved="no">
|
||||
<source>Archive and upload</source>
|
||||
<target state="translated">Arquivar e enviar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Background" xml:space="preserve" approved="no">
|
||||
<source>Background</source>
|
||||
<target state="translated">Fundo</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Better message dates." xml:space="preserve" approved="no">
|
||||
<source>Better message dates.</source>
|
||||
<target state="translated">Datas de mensagens melhores.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Better notifications" xml:space="preserve" approved="no">
|
||||
<source>Better notifications</source>
|
||||
<target state="translated">Notificações melhores</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Better security ✅" xml:space="preserve" approved="no">
|
||||
<source>Better security ✅</source>
|
||||
<target state="translated">Melhor segurança ✅</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat profile" xml:space="preserve" approved="no">
|
||||
<source>Chat profile</source>
|
||||
<target state="translated">Perfil da conversa</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt-BR" datatype="plaintext">
|
||||
|
||||
@@ -214,22 +214,17 @@ Available in v5.1</source>
|
||||
<source>)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<target state="translated">**Adicionar novo contato**: para criar seu QR Code único ou link para seu contato.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||
<source>**Create link / QR code** for your contact to use.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
||||
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||
<target state="translated">**Mais privado**: verifique novas mensagens a cada 20 minutos. O token do dispositivo é compartilhado com o servidor SimpleX Chat, mas não com quantos contatos ou mensagens você possui.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||
<target state="translated">**Totalmente privado**: não use o servidor de notificações do SimpleX Chat, verifique as mensagens periodicamente em segundo plano (depende da frequência com que você usa o aplicativo).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -242,8 +237,8 @@ Available in v5.1</source>
|
||||
<target state="translated">**Atenção**: Você NÃO poderá recuperar ou alterar a senha caso a perca.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||
<target state="translated">**Recomendado**: O token do dispositivo e as notificações são enviados ao servidor de notificação do SimpleX Chat, mas não o conteúdo, o tamanho da mensagem ou de quem ela é.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
@@ -1232,8 +1227,8 @@ Available in v5.1</source>
|
||||
<source>Direct messages</source>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited in this group.</source>
|
||||
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||
<source>Direct messages between members are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||
@@ -1248,8 +1243,8 @@ Available in v5.1</source>
|
||||
<source>Disappearing messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited in this group.</source>
|
||||
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||
<source>Disappearing messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -1684,16 +1679,16 @@ Available in v5.1</source>
|
||||
<source>Group members can irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||
<source>Members can send direct messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
||||
<source>Group members can send disappearing messages.</source>
|
||||
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||
<source>Members can send disappearing messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||
<source>Group members can send voice messages.</source>
|
||||
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||
<source>Members can send voice messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
@@ -1812,8 +1807,8 @@ Available in v5.1</source>
|
||||
<source>Immediately</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
<source>Immune to spam and abuse</source>
|
||||
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||
<source>Immune to spam</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import" xml:space="preserve">
|
||||
@@ -1925,8 +1920,8 @@ Available in v5.1</source>
|
||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||
<source>Irreversible message deletion is prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||
@@ -2120,8 +2115,8 @@ Available in v5.1</source>
|
||||
<source>Migration is completed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrations: %@" xml:space="preserve">
|
||||
<source>Migrations: %@</source>
|
||||
<trans-unit id="Migrations:" xml:space="preserve">
|
||||
<source>Migrations:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderate" xml:space="preserve">
|
||||
@@ -2278,8 +2273,8 @@ Available in v5.1</source>
|
||||
<source>Onion hosts will not be used.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||
@@ -2338,8 +2333,8 @@ Available in v5.1</source>
|
||||
<source>Open user profiles</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||
<source>Anybody can host servers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -2394,8 +2389,8 @@ Available in v5.1</source>
|
||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
||||
<source>People can connect to you only via the links you share.</source>
|
||||
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||
<source>You decide who can connect.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Periodically" xml:space="preserve">
|
||||
@@ -3098,8 +3093,8 @@ Available in v5.1</source>
|
||||
<source>Thanks to the users – contribute via Weblate!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
||||
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||
<source>No user identifiers.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
||||
@@ -3143,8 +3138,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>The message will be marked as moderated for all members.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
||||
<source>The next generation of private messaging</source>
|
||||
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||
<source>The future of messaging</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||
@@ -3215,8 +3210,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<source>To make a new connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||
@@ -3441,8 +3436,8 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Voice messages are prohibited in this chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||
<source>Voice messages are prohibited in this group.</source>
|
||||
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||
<source>Voice messages are prohibited.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
@@ -3582,10 +3577,6 @@ SimpleX Lock must be enabled.</source>
|
||||
<source>You can't send messages!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||
<source>You could not be verified; please try again.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -4302,8 +4293,8 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target state="translated">%lld novas interface de idiomas</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve" approved="no">
|
||||
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
|
||||
<trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no">
|
||||
<source>**Create 1-time link**: to create and share a new invitation link.</source>
|
||||
<target state="translated">**Adicionar contato**: para criar um novo link de convite ou conectar-se por meio de um link que você recebeu.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"_italic_" = "\\_italic_";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"*bold*" = "\\*bold*";
|
||||
|
||||
@@ -27,4 +24,3 @@
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"No group!" = "Group not found!";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user