diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c1a1d5d7db..5be607567f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -43,7 +43,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
- name: build-${{ matrix.os }}
+ name: build-${{ matrix.os }}-${{ matrix.ghc }}
if: always()
needs: prepare-release
runs-on: ${{ matrix.os }}
@@ -52,18 +52,25 @@ jobs:
matrix:
include:
- os: ubuntu-20.04
+ ghc: "8.10.7"
+ cache_path: ~/.cabal/store
+ - os: ubuntu-20.04
+ ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-20_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb
- os: ubuntu-22.04
+ ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-22_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb
- os: macos-latest
+ ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-macos-x86-64
desktop_asset_name: simplex-desktop-macos-x86_64.dmg
- os: windows-latest
+ ghc: "9.6.3"
cache_path: C:/cabal
asset_name: simplex-chat-windows-x86-64
desktop_asset_name: simplex-desktop-windows-x86_64.msi
@@ -82,16 +89,17 @@ jobs:
- name: Setup Haskell
uses: haskell/actions/setup@v2
with:
- ghc-version: "8.10.7"
- cabal-version: "latest"
+ ghc-version: ${{ matrix.ghc }}
+ cabal-version: "3.10.1.0"
- - name: Cache dependencies
- uses: actions/cache@v3
+ - name: Restore cached build
+ id: restore_cache
+ uses: actions/cache/restore@v3
with:
path: |
${{ matrix.cache_path }}
dist-newstyle
- key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
+ key: ${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
# / Unix
@@ -106,7 +114,7 @@ jobs:
echo " flags: +openssl" >> cabal.project.local
- name: Install AppImage dependencies
- if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
run: sudo apt install -y desktop-file-utils
- name: Install pkg-config for Mac
@@ -132,7 +140,7 @@ jobs:
echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
- name: Unix upload CLI binary to release
- if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os != 'windows-latest'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -141,7 +149,7 @@ jobs:
tag: ${{ github.ref }}
- name: Unix update CLI binary hash
- if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os != 'windows-latest'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -151,7 +159,7 @@ jobs:
${{ steps.unix_cli_build.outputs.bin_hash }}
- name: Setup Java
- if: startsWith(github.ref, 'refs/tags/v')
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name
uses: actions/setup-java@v3
with:
distribution: 'corretto'
@@ -160,7 +168,7 @@ jobs:
- name: Linux build desktop
id: linux_desktop_build
- if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
shell: bash
run: |
scripts/desktop/build-lib-linux.sh
@@ -169,10 +177,10 @@ jobs:
path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb)
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
-
+
- name: Linux make AppImage
id: linux_appimage_build
- if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
shell: bash
run: |
scripts/desktop/make-appimage-linux.sh
@@ -195,7 +203,7 @@ jobs:
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
- name: Linux upload desktop package to release
- if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -204,7 +212,7 @@ jobs:
tag: ${{ github.ref }}
- name: Linux update desktop package hash
- if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -214,7 +222,7 @@ jobs:
${{ steps.linux_desktop_build.outputs.package_hash }}
- name: Linux upload AppImage to release
- if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -223,7 +231,7 @@ jobs:
tag: ${{ github.ref }}
- name: Linux update AppImage hash
- if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
+ if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -251,6 +259,15 @@ jobs:
body: |
${{ steps.mac_desktop_build.outputs.package_hash }}
+ - name: Cache unix build
+ uses: actions/cache/save@v3
+ if: matrix.os != 'windows-latest'
+ with:
+ path: |
+ ${{ matrix.cache_path }}
+ dist-newstyle
+ key: ${{ steps.restore_cache.outputs.cache-primary-key }}
+
- name: Unix test
if: matrix.os != 'windows-latest'
timeout-minutes: 30
@@ -284,7 +301,7 @@ jobs:
if: matrix.os == 'windows-latest'
shell: msys2 {0}
run: |
- export PATH=$PATH:/c/ghcup/bin
+ export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
scripts/desktop/prepare-openssl-windows.sh
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
rm cabal.project.local 2>/dev/null || true
@@ -326,14 +343,14 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
shell: msys2 {0}
run: |
- export PATH=$PATH:/c/ghcup/bin
+ export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
scripts/desktop/build-lib-windows.sh
cd apps/multiplatform
./gradlew packageMsi
path=$(echo $PWD/release/main/msi/*imple*.msi | sed 's#/\([a-z]\)#\1:#' | sed 's#/#\\#g')
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
-
+
- name: Windows upload desktop package to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
uses: svenstaro/upload-release-action@v2
@@ -353,4 +370,13 @@ jobs:
body: |
${{ steps.windows_desktop_build.outputs.package_hash }}
+ - name: Cache windows build
+ uses: actions/cache/save@v3
+ if: matrix.os == 'windows-latest'
+ with:
+ path: |
+ ${{ matrix.cache_path }}
+ dist-newstyle
+ key: ${{ steps.restore_cache.outputs.cache-primary-key }}
+
# Windows /
diff --git a/README.md b/README.md
index 254a66c2bc..f6630ebbfd 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ You must:
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
-You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-4](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fw2GlucRXtRVgYnbt_9ZP-kmt76DekxxS%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0tJhTyMGUxznwmjb7aT24P1I1Wry_iURTuhOFlMb1Eo%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22WoPxjFqGEDlVazECOSi2dg%3D%3D%22%7D)
+You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform:
@@ -127,6 +127,7 @@ Join our translators to help SimpleX grow!
|🇫🇮 fi|Suomi | |[](https://hosted.weblate.org/projects/simplex-chat/android/fi/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[](https://hosted.weblate.org/projects/simplex-chat/website/fi/)||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇮🇱 he|עִברִית | |[](https://hosted.weblate.org/projects/simplex-chat/android/he/)
-|||
+|🇭🇺 hu|Magyar | |[](https://hosted.weblate.org/projects/simplex-chat/android/hu/)
-|||
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇯🇵 ja|日本語 | |[](https://hosted.weblate.org/projects/simplex-chat/android/ja/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[](https://hosted.weblate.org/projects/simplex-chat/website/ja/)||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
@@ -134,6 +135,7 @@ Join our translators to help SimpleX grow!
|🇧🇷 pt-BR|Português||[](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)
-|[](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[](https://hosted.weblate.org/projects/simplex-chat/android/th/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/th/)|||
+|🇹🇷 tr|Türkçe | |[](https://hosted.weblate.org/projects/simplex-chat/android/tr/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/tr/)|||
|🇺🇦 uk|Українська| |[](https://hosted.weblate.org/projects/simplex-chat/android/uk/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[](https://hosted.weblate.org/projects/simplex-chat/website/uk/)||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)
[Float-hu](https://github.com/Float-hu)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)
[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)
|
[](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
@@ -232,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates:
+[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).
@@ -297,7 +301,7 @@ What is already implemented:
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
12. Manual messaging queue rotations to move conversation to another SMP relay.
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
-14. Local files encryption, except videos (to be added later).
+14. Local files encryption.
We plan to add:
@@ -369,12 +373,13 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Desktop client.
- ✅ Encryption of local files stored in the app.
- ✅ Using mobile profiles from the desktop app.
+- ✅ Private notes.
+- ✅ Improve sending videos (including encryption of locally stored videos).
- 🏗 Improve experience for the new users.
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
- 🏗 Large groups, communities and public channels.
-- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
+- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- Privacy & security slider - a simple way to set all settings at once.
-- Improve sending videos (including encryption of locally stored videos).
- SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift
index 145e362797..24c0eeb605 100644
--- a/apps/ios/Shared/AppDelegate.swift
+++ b/apps/ios/Shared/AppDelegate.swift
@@ -15,6 +15,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
if #available(iOS 17.0, *) { trackKeyboard() }
+ NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
return true
}
@@ -36,6 +37,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
ChatModel.shared.keyboardHeight = 0
}
+ @objc func pasteboardChanged() {
+ ChatModel.shared.pasteboardHasStrings = UIPasteboard.general.hasStrings
+ }
+
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift
index d7b9fef218..45e0332dab 100644
--- a/apps/ios/Shared/ContentView.swift
+++ b/apps/ios/Shared/ContentView.swift
@@ -31,6 +31,7 @@ struct ContentView: View {
@State private var showWhatsNew = false
@State private var showChooseLAMode = false
@State private var showSetPasscode = false
+ @State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil
private enum ChatListActionSheet: Identifiable {
@@ -61,6 +62,10 @@ struct ContentView: View {
}
if !showSettings, let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
+ .onDisappear {
+ // this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
+ waitingForOrPassedAuth = accessAuthenticated
+ }
} else if showSetPasscode {
SetAppPasscodeView {
chatModel.contentViewAccessAuthenticated = true
@@ -73,8 +78,7 @@ struct ContentView: View {
showSetPasscode = false
alertManager.showAlert(laPasscodeNotSetAlert())
}
- }
- if chatModel.chatDbStatus == nil {
+ } else if chatModel.chatDbStatus == nil && AppChatState.shared.value != .stopped && waitingForOrPassedAuth {
initializationView()
}
}
@@ -182,7 +186,7 @@ struct ContentView: View {
.onAppear {
requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete
- if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
+ if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) {
prefLANoticeShown = true
alertManager.showAlert(laNoticeAlert())
} else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil {
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index 0cc281fda9..c31ad579ab 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -54,11 +54,13 @@ final class ChatModel: ObservableObject {
@Published var chatDbChanged = false
@Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult?
+ @Published var ctrlInitInProgress: Bool = false
// local authentication
@Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest?
// list of chat "previews"
@Published var chats: [Chat] = []
+ @Published var deletedChats: Set = []
// map of connections network statuses, key is agent connection id
@Published var networkStatuses: Dictionary = [:]
// current chat
@@ -89,14 +91,15 @@ final class ChatModel: ObservableObject {
@Published var showCallView = false
// remote desktop
@Published var remoteCtrlSession: RemoteCtrlSession?
- // currently showing QR code
- @Published var connReqInv: String?
+ // currently showing invitation
+ @Published var showingInvitation: ShowingInvitation?
// audio recording and playback
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@Published var draftChatId: String?
// tracks keyboard height via subscription in AppDelegate
@Published var keyboardHeight: CGFloat = 0
+ @Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
var messageDelivery: Dictionary Void> = [:]
@@ -136,7 +139,7 @@ final class ChatModel: ObservableObject {
}
func removeUser(_ user: User) {
- if let i = getUserIndex(user), users[i].user.userId != currentUser?.userId {
+ if let i = getUserIndex(user) {
users.remove(at: i)
}
}
@@ -620,14 +623,16 @@ final class ChatModel: ObservableObject {
}
func dismissConnReqView(_ id: String) {
- if let connReqInv = connReqInv,
- let c = getChat(id),
- case let .contactConnection(contactConnection) = c.chatInfo,
- connReqInv == contactConnection.connReqInv {
+ if id == showingInvitation?.connId {
+ markShowingInvitationUsed()
dismissAllSheets()
}
}
+ func markShowingInvitationUsed() {
+ showingInvitation?.connChatUsed = true
+ }
+
func removeChat(_ id: String) {
withAnimation {
chats.removeAll(where: { $0.id == id })
@@ -704,6 +709,11 @@ final class ChatModel: ObservableObject {
}
}
+struct ShowingInvitation {
+ var connId: String
+ var connChatUsed: Bool
+}
+
struct NTFContactRequest {
var incognito: Bool
var chatId: String
@@ -746,6 +756,8 @@ final class Chat: ObservableObject, Identifiable {
case let .group(groupInfo):
let m = groupInfo.membership
return m.memberActive && m.memberRole >= .member
+ case .local:
+ return true
default: return false
}
}
diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/Shared/Model/ImageUtils.swift
index 41d741e7e6..6437597b19 100644
--- a/apps/ios/Shared/Model/ImageUtils.swift
+++ b/apps/ios/Shared/Model/ImageUtils.swift
@@ -158,7 +158,8 @@ func imageHasAlpha(_ img: UIImage) -> Bool {
return false
}
-func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? {
+func saveFileFromURL(_ url: URL) -> CryptoFile? {
+ let encrypted = privacyEncryptLocalFilesGroupDefault.get()
let savedFile: CryptoFile?
if url.startAccessingSecurityScopedResource() {
do {
@@ -185,10 +186,19 @@ func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? {
func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
do {
+ let encrypted = privacyEncryptLocalFilesGroupDefault.get()
let fileName = uniqueCombine(url.lastPathComponent)
- try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
+ let savedFile: CryptoFile?
+ if encrypted {
+ let cfArgs = try encryptCryptoFile(fromPath: url.path, toPath: getAppFilePath(fileName).path)
+ try FileManager.default.removeItem(atPath: url.path)
+ savedFile = CryptoFile(filePath: fileName, cryptoArgs: cfArgs)
+ } else {
+ try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
+ savedFile = CryptoFile.plain(fileName)
+ }
ChatModel.shared.filesToDelete.remove(url)
- return CryptoFile.plain(fileName)
+ return savedFile
} catch {
logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)")
return nil
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 6a5de6b25b..24a77cb3d3 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -365,6 +365,13 @@ func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId:
}
}
+func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? {
+ let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg))
+ if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem }
+ createChatItemErrorAlert(r)
+ return nil
+}
+
private func sendMessageErrorAlert(_ r: ChatResponse) {
logger.error("apiSendMessage error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(
@@ -373,6 +380,14 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
)
}
+private func createChatItemErrorAlert(_ r: ChatResponse) {
+ logger.error("apiCreateChatItem error: \(String(describing: r))")
+ AlertManager.shared.showAlertMsg(
+ title: "Error creating message",
+ message: "Error: \(String(describing: r))"
+ )
+}
+
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem {
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay)
if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem }
@@ -581,15 +596,15 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo
return nil
}
-func apiAddContact(incognito: Bool) async -> (String, PendingContactConnection)? {
+func apiAddContact(incognito: Bool) async -> ((String, PendingContactConnection)?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiAddContact: no current user")
- return nil
+ return (nil, nil)
}
let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false)
- if case let .invitation(_, connReqInvitation, connection) = r { return (connReqInvitation, connection) }
- AlertManager.shared.showAlert(connectionErrorAlert(r))
- return nil
+ if case let .invitation(_, connReqInvitation, connection) = r { return ((connReqInvitation, connection), nil) }
+ let alert = connectionErrorAlert(r)
+ return (nil, alert)
}
func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? {
@@ -691,6 +706,9 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co
}
func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws {
+ let chatId = type.rawValue + id.description
+ DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) }
+ defer { DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } }
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false)
if case .direct = type, case .contactDeleted = r { return }
if case .contactConnection = type, case .contactConnectionDeleted = r { return }
@@ -852,8 +870,8 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
-func receiveFile(user: any UserLike, fileId: Int64, encrypted: Bool, auto: Bool = false) async {
- if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: encrypted, auto: auto) {
+func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
+ if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
await chatItemSimpleUpdate(user, chatItem)
}
}
@@ -1123,6 +1141,12 @@ func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMembe
throw r
}
+func apiBlockMemberForAll(_ groupId: Int64, _ memberId: Int64, _ blocked: Bool) async throws -> GroupMember {
+ let r = await chatSendCmd(.apiBlockMemberForAll(groupId: groupId, memberId: memberId, blocked: blocked), bgTask: false)
+ if case let .memberBlockedForAllUser(_, _, member, _) = r { return member }
+ throw r
+}
+
func leaveGroup(_ groupId: Int64) async {
do {
let groupInfo = try await apiLeaveGroup(groupId)
@@ -1212,9 +1236,11 @@ private func currentUserId(_ funcName: String) throws -> Int64 {
throw RuntimeError("\(funcName): no current user")
}
-func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
+func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = nil, refreshInvitations: Bool = true, confirmMigrations: MigrationConfirmation? = nil) throws {
logger.debug("initializeChat")
let m = ChatModel.shared
+ m.ctrlInitInProgress = true
+ defer { m.ctrlInitInProgress = false }
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
if m.chatDbStatus != .ok { return }
// If we migrated successfully means previous re-encryption process on database level finished successfully too
@@ -1231,7 +1257,37 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
onboardingStageDefault.set(.step1_SimpleXInfo)
privacyDeliveryReceiptsSet.set(true)
m.onboardingStage = .step1_SimpleXInfo
- } else if start {
+ } else if confirmStart {
+ showStartChatAfterRestartAlert { start in
+ do {
+ if start { AppChatState.shared.set(.active) }
+ try chatInitialized(start: start, refreshInvitations: refreshInvitations)
+ } catch let error {
+ logger.error("ChatInitialized error: \(error)")
+ }
+ }
+ } else {
+ try chatInitialized(start: start, refreshInvitations: refreshInvitations)
+ }
+}
+
+func showStartChatAfterRestartAlert(result: @escaping (_ start: Bool) -> Void) {
+ AlertManager.shared.showAlert(Alert(
+ title: Text("Start chat?"),
+ message: Text("Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat."),
+ primaryButton: .default(Text("Ok")) {
+ result(true)
+ },
+ secondaryButton: .cancel {
+ result(false)
+ }
+ ))
+}
+
+private func chatInitialized(start: Bool, refreshInvitations: Bool) throws {
+ let m = ChatModel.shared
+ if m.currentUser == nil { return }
+ if start {
try startChat(refreshInvitations: refreshInvitations)
} else {
m.chatRunning = false
@@ -1289,8 +1345,12 @@ private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws {
try getUserChatData()
}
-func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws {
- let currentUser = try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
+func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws {
+ let currentUser = if let userId = userId {
+ try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
+ } else {
+ try apiGetActiveUser()
+ }
let users = try await listUsersAsync()
await MainActor.run {
let m = ChatModel.shared
@@ -1299,7 +1359,7 @@ func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws {
}
try await getUserChatDataAsync()
await MainActor.run {
- if var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
+ if let currentUser = currentUser, var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
invitation.user = currentUser
activateCall(invitation)
}
@@ -1315,14 +1375,21 @@ func getUserChatData() throws {
}
private func getUserChatDataAsync() async throws {
- let userAddress = try await apiGetUserAddressAsync()
- let chatItemTTL = try await getChatItemTTLAsync()
- let chats = try await apiGetChatsAsync()
- await MainActor.run {
- let m = ChatModel.shared
- m.userAddress = userAddress
- m.chatItemTTL = chatItemTTL
- m.chats = chats.map { Chat.init($0) }
+ let m = ChatModel.shared
+ if m.currentUser != nil {
+ let userAddress = try await apiGetUserAddressAsync()
+ let chatItemTTL = try await getChatItemTTLAsync()
+ let chats = try await apiGetChatsAsync()
+ await MainActor.run {
+ m.userAddress = userAddress
+ m.chatItemTTL = chatItemTTL
+ m.chats = chats.map { Chat.init($0) }
+ }
+ } else {
+ await MainActor.run {
+ m.userAddress = nil
+ m.chats = []
+ }
}
}
@@ -1481,7 +1548,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
if let file = cItem.autoReceiveFile() {
Task {
- await receiveFile(user: user, fileId: file.fileId, encrypted: cItem.encryptLocalFile, auto: true)
+ await receiveFile(user: user, fileId: file.fileId, auto: true)
}
}
if cItem.showNotification {
@@ -1619,6 +1686,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
_ = m.upsertGroupMember(groupInfo, member)
}
}
+ case let .memberBlockedForAll(user, groupInfo, byMember: _, member: member, blocked: _):
+ if active(user) {
+ await MainActor.run {
+ m.updateGroup(groupInfo)
+ _ = m.upsertGroupMember(groupInfo, member)
+ }
+ }
case let .newMemberContactReceivedInv(user, contact, _, _):
if active(user) {
await MainActor.run {
diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift
index 0229bff2b3..4494adc0e8 100644
--- a/apps/ios/Shared/Model/SuspendChat.swift
+++ b/apps/ios/Shared/Model/SuspendChat.swift
@@ -107,26 +107,16 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
let m = ChatModel.shared
if (!m.chatInitialized) {
m.v3DBMigration = v3DBMigrationDefault.get()
- if AppChatState.shared.value == .stopped {
- AlertManager.shared.showAlert(Alert(
- title: Text("Start chat?"),
- message: Text("Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat."),
- primaryButton: .default(Text("Ok")) {
- AppChatState.shared.set(.active)
- initialize(start: true)
- },
- secondaryButton: .cancel {
- initialize(start: false)
- }
- ))
+ if AppChatState.shared.value == .stopped && storeDBPassphraseGroupDefault.get() && kcDatabasePassword.get() != nil {
+ initialize(start: true, confirmStart: true)
} else {
initialize(start: true)
}
}
- func initialize(start: Bool) {
+ func initialize(start: Bool, confirmStart: Bool = false) {
do {
- try initializeChat(start: m.v3DBMigration.startChat && start, refreshInvitations: refreshInvitations)
+ try initializeChat(start: m.v3DBMigration.startChat && start, confirmStart: m.v3DBMigration.startChat && confirmStart, refreshInvitations: refreshInvitations)
} catch let error {
AlertManager.shared.showAlertMsg(
title: start ? "Error starting chat" : "Error opening chat",
diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift
index 60d1cf7256..e5b98589a0 100644
--- a/apps/ios/Shared/SimpleXApp.swift
+++ b/apps/ios/Shared/SimpleXApp.swift
@@ -44,8 +44,10 @@ struct SimpleXApp: App {
chatModel.appOpenUrl = url
}
.onAppear() {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
- initChatAndMigrate()
+ if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
+ initChatAndMigrate()
+ }
}
}
.onChange(of: scenePhase) { phase in
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
index 4ae2296f46..c94ba3f830 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
@@ -52,7 +52,7 @@ struct CIFileView: View {
private var itemInteractive: Bool {
if let file = file {
switch (file.fileStatus) {
- case .sndStored: return false
+ case .sndStored: return file.fileProtocol == .local
case .sndTransfer: return false
case .sndComplete: return false
case .sndCancelled: return false
@@ -85,8 +85,7 @@ struct CIFileView: View {
Task {
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
if let user = m.currentUser {
- let encrypted = privacyEncryptLocalFilesGroupDefault.get()
- await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted)
+ await receiveFile(user: user, fileId: file.fileId)
}
}
} else {
@@ -108,12 +107,18 @@ struct CIFileView: View {
title: "Waiting for file",
message: "File will be received when your contact is online, please wait or check later!"
)
+ case .local: ()
}
case .rcvComplete:
logger.debug("CIFileView fileAction - in .rcvComplete")
if let fileSource = getLoadedFileSource(file) {
saveCryptoFile(fileSource)
}
+ case .sndStored:
+ logger.debug("CIFileView fileAction - in .sndStored")
+ if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
+ saveCryptoFile(fileSource)
+ }
default: break
}
}
@@ -126,11 +131,13 @@ struct CIFileView: View {
switch file.fileProtocol {
case .xftp: progressView()
case .smp: fileIcon("doc.fill")
+ case .local: fileIcon("doc.fill")
}
case let .sndTransfer(sndProgress, sndTotal):
switch file.fileProtocol {
case .xftp: progressCircle(sndProgress, sndTotal)
case .smp: progressView()
+ case .local: EmptyView()
}
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
index 9ae52ae01b..c7e89fc5ed 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
@@ -38,7 +38,7 @@ struct CIImageView: View {
case .rcvInvitation:
Task {
if let user = m.currentUser {
- await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile)
+ await receiveFile(user: user, fileId: file.fileId)
}
}
case .rcvAccepted:
@@ -53,6 +53,7 @@ struct CIImageView: View {
title: "Waiting for image",
message: "Image will be received when your contact is online, please wait or check later!"
)
+ case .local: ()
}
case .rcvTransfer: () // ?
case .rcvComplete: () // ?
@@ -90,6 +91,7 @@ struct CIImageView: View {
switch file.fileProtocol {
case .xftp: progressView()
case .smp: EmptyView()
+ case .local: EmptyView()
}
case .sndTransfer: progressView()
case .sndComplete: fileIcon("checkmark", 10, 13)
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
index be8b25a0fc..a824ddc49f 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift
@@ -26,6 +26,8 @@ struct CIVideoView: View {
@State private var player: AVPlayer?
@State private var fullPlayer: AVPlayer?
@State private var url: URL?
+ @State private var urlDecrypted: URL?
+ @State private var decryptionInProgress: Bool = false
@State private var showFullScreenPlayer = false
@State private var timeObserver: Any? = nil
@State private var fullScreenTimeObserver: Any? = nil
@@ -39,8 +41,12 @@ struct CIVideoView: View {
self._videoWidth = videoWidth
self.scrollProxy = scrollProxy
if let url = getLoadedVideo(chatItem.file) {
- self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(url, false))
- self._fullPlayer = State(initialValue: AVPlayer(url: url))
+ let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet()
+ self._urlDecrypted = State(initialValue: decrypted)
+ if let decrypted = decrypted {
+ self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(decrypted, false))
+ self._fullPlayer = State(initialValue: AVPlayer(url: decrypted))
+ }
self._url = State(initialValue: url)
}
if let data = Data(base64Encoded: dropImagePrefix(image)),
@@ -53,8 +59,10 @@ struct CIVideoView: View {
let file = chatItem.file
ZStack {
ZStack(alignment: .topLeading) {
- if let file = file, let preview = preview, let player = player, let url = url {
- videoView(player, url, file, preview, duration)
+ if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted {
+ videoView(player, decrypted, file, preview, duration)
+ } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil {
+ videoViewEncrypted(file, defaultPreview, duration)
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
imageView(uiImage)
@@ -62,7 +70,7 @@ struct CIVideoView: View {
if let file = file {
switch file.fileStatus {
case .rcvInvitation:
- receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
+ receiveFileIfValidSize(file: file, receiveFile: receiveFile)
case .rcvAccepted:
switch file.fileProtocol {
case .xftp:
@@ -75,6 +83,7 @@ struct CIVideoView: View {
title: "Waiting for video",
message: "Video will be received when your contact is online, please wait or check later!"
)
+ case .local: ()
}
case .rcvTransfer: () // ?
case .rcvComplete: () // ?
@@ -88,7 +97,7 @@ struct CIVideoView: View {
}
if let file = file, case .rcvInvitation = file.fileStatus {
Button {
- receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile)
+ receiveFileIfValidSize(file: file, receiveFile: receiveFile)
} label: {
playPauseIcon("play.fill")
}
@@ -96,12 +105,46 @@ struct CIVideoView: View {
}
}
+ private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
+ return ZStack(alignment: .topTrailing) {
+ ZStack(alignment: .center) {
+ let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
+ imageView(defaultPreview)
+ .fullScreenCover(isPresented: $showFullScreenPlayer) {
+ if let decrypted = urlDecrypted {
+ fullScreenPlayer(decrypted)
+ }
+ }
+ .onTapGesture {
+ decrypt(file: file) {
+ showFullScreenPlayer = urlDecrypted != nil
+ }
+ }
+ if !decryptionInProgress {
+ Button {
+ decrypt(file: file) {
+ if let decrypted = urlDecrypted {
+ videoPlaying = true
+ player?.play()
+ }
+ }
+ } label: {
+ playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
+ }
+ .disabled(!canBePlayed)
+ } else {
+ videoDecryptionProgress()
+ }
+ }
+ }
+ }
+
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
DispatchQueue.main.async { videoWidth = w }
return ZStack(alignment: .topTrailing) {
ZStack(alignment: .center) {
- let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete
+ let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local)
VideoPlayerView(player: player, url: url, showControls: false)
.frame(width: w, height: w * preview.size.height / preview.size.width)
.onChange(of: m.stopPreviousRecPlay) { playingUrl in
@@ -159,6 +202,16 @@ struct CIVideoView: View {
.clipShape(Circle())
}
+ private func videoDecryptionProgress(_ color: Color = .white) -> some View {
+ ProgressView()
+ .progressViewStyle(.circular)
+ .frame(width: 12, height: 12)
+ .tint(color)
+ .frame(width: 40, height: 40)
+ .background(Color.black.opacity(0.35))
+ .clipShape(Circle())
+ }
+
private func durationProgress() -> some View {
HStack {
Text("\(durationText(videoPlaying ? progress : duration))")
@@ -202,11 +255,13 @@ struct CIVideoView: View {
switch file.fileProtocol {
case .xftp: progressView()
case .smp: EmptyView()
+ case .local: EmptyView()
}
case let .sndTransfer(sndProgress, sndTotal):
switch file.fileProtocol {
case .xftp: progressCircle(sndProgress, sndTotal)
case .smp: progressView()
+ case .local: EmptyView()
}
case .sndComplete: fileIcon("checkmark", 10, 13)
case .sndCancelled: fileIcon("xmark", 10, 13)
@@ -257,10 +312,10 @@ struct CIVideoView: View {
}
// TODO encrypt: where file size is checked?
- private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
+ private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
Task {
if let user = m.currentUser {
- await receiveFile(user, file.fileId, encrypted, false)
+ await receiveFile(user, file.fileId, false)
}
}
}
@@ -323,6 +378,22 @@ struct CIVideoView: View {
}
}
+ private func decrypt(file: CIFile, completed: (() -> Void)? = nil) {
+ if decryptionInProgress { return }
+ decryptionInProgress = true
+ Task {
+ urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete)
+ await MainActor.run {
+ if let decrypted = urlDecrypted {
+ player = VideoPlayerView.getOrCreatePlayer(decrypted, false)
+ fullPlayer = AVPlayer(url: decrypted)
+ }
+ decryptionInProgress = true
+ completed?()
+ }
+ }
+ }
+
private func addObserver(_ player: AVPlayer, _ url: URL) {
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { time in
if let item = player.currentItem {
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
index 2e54ba4143..3aecb65ebd 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
@@ -221,7 +221,7 @@ struct VoiceMessagePlayer: View {
Button {
Task {
if let user = chatModel.currentUser {
- await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get())
+ await receiveFile(user: user, fileId: recordingFile.fileId)
}
}
} label: {
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
index 51dfa3cb50..f7775a7cdd 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
@@ -9,6 +9,8 @@
import SwiftUI
import SimpleXChat
+let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21)
+let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19)
let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12)
let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17)
private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11)
@@ -28,7 +30,9 @@ struct FramedItemView: View {
@State var metaColor = Color.secondary
@State var showFullScreenImage = false
@Binding var allowMenu: Bool
-
+ @State private var showSecrets = false
+ @State private var showQuoteSecrets = false
+
@Binding var audioPlayer: AudioPlayer?
@Binding var playbackState: VoiceMessagePlaybackState
@Binding var playbackTime: TimeInterval?
@@ -42,7 +46,9 @@ struct FramedItemView: View {
framedItemHeader(icon: "flag", caption: Text("moderated by \(byGroupMember.displayName)").italic())
case .blocked:
framedItemHeader(icon: "hand.raised", caption: Text("blocked").italic())
- default:
+ case .blockedByAdmin:
+ framedItemHeader(icon: "hand.raised", caption: Text("blocked by admin").italic())
+ case .deleted:
framedItemHeader(icon: "trash", caption: Text("marked deleted").italic())
}
} else if chatItem.meta.isLive {
@@ -252,10 +258,12 @@ struct FramedItemView: View {
}
private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View {
- MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText)
- .lineLimit(lines)
- .font(.subheadline)
- .padding(.bottom, 6)
+ toggleSecrets(qi.formattedText, $showQuoteSecrets,
+ MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, showSecrets: showQuoteSecrets)
+ .lineLimit(lines)
+ .font(.subheadline)
+ .padding(.bottom, 6)
+ )
}
private func ciQuoteIconView(_ image: String) -> some View {
@@ -278,13 +286,15 @@ struct FramedItemView: View {
@ViewBuilder private func ciMsgContentView(_ ci: ChatItem) -> some View {
let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text
let rtl = isRightToLeft(text)
- let v = MsgContentView(
+ let ft = text == "" ? [] : ci.formattedText
+ let v = toggleSecrets(ft, $showSecrets, MsgContentView(
chat: chat,
text: text,
- formattedText: text == "" ? [] : ci.formattedText,
+ formattedText: ft,
meta: ci.meta,
- rightToLeft: rtl
- )
+ rightToLeft: rtl,
+ showSecrets: showSecrets
+ ))
.multilineTextAlignment(rtl ? .trailing : .leading)
.padding(.vertical, 6)
.padding(.horizontal, 12)
@@ -298,7 +308,7 @@ struct FramedItemView: View {
v
}
}
-
+
@ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View {
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
.overlay(DetermineWidth())
@@ -318,6 +328,14 @@ struct FramedItemView: View {
}
}
+@ViewBuilder func toggleSecrets(_ ft: [FormattedText]?, _ showSecrets: Binding, _ v: V) -> some View {
+ if let ft = ft, ft.contains(where: { $0.isSecret }) {
+ v.onTapGesture { showSecrets.wrappedValue.toggle() }
+ } else {
+ v
+ }
+}
+
func isRightToLeft(_ s: String) -> Bool {
if let lang = CFStringTokenizerCopyBestStringLanguage(s as CFString, CFRange(location: 0, length: min(s.count, 80))) {
return NSLocale.characterDirection(forLanguage: lang as String) == .rightToLeft
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
index c6af95e6f6..dfa4a97fc2 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
@@ -33,6 +33,7 @@ struct MarkedDeletedItemView: View {
var i = m.getChatItemIndex(chatItem) {
var moderated = 0
var blocked = 0
+ var blockedByAdmin = 0
var deleted = 0
var moderatedBy: Set = []
while i < m.reversedChatItems.count,
@@ -44,16 +45,19 @@ struct MarkedDeletedItemView: View {
moderated += 1
moderatedBy.insert(byGroupMember.displayName)
case .blocked: blocked += 1
+ case .blockedByAdmin: blockedByAdmin += 1
case .deleted: deleted += 1
}
i += 1
}
- let total = moderated + blocked + deleted
+ let total = moderated + blocked + blockedByAdmin + deleted
return total <= 1
? markedDeletedText
: total == moderated
? "\(total) messages moderated by \(moderatedBy.joined(separator: ", "))"
- : total == blocked
+ : total == blockedByAdmin
+ ? "\(total) messages blocked by admin"
+ : total == blocked + blockedByAdmin
? "\(total) messages blocked"
: "\(total) messages marked deleted"
} else {
@@ -65,7 +69,8 @@ struct MarkedDeletedItemView: View {
switch chatItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
case .blocked: "blocked"
- default: "marked deleted"
+ case .blockedByAdmin: "blocked by admin"
+ case .deleted, nil: "marked deleted"
}
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
index d0d2bdf3dd..ccd7ac0a12 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
@@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
-private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
+let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
private let noTyping = Text(" ")
@@ -31,6 +31,7 @@ struct MsgContentView: View {
var sender: String? = nil
var meta: CIMeta? = nil
var rightToLeft = false
+ var showSecrets: Bool
@State private var typingIdx = 0
@State private var timer: Timer?
@@ -62,7 +63,7 @@ struct MsgContentView: View {
}
private func msgContentView() -> Text {
- var v = messageText(text, formattedText, sender)
+ var v = messageText(text, formattedText, sender, showSecrets: showSecrets)
if let mt = meta {
if mt.isLive {
v = v + typingIndicator(mt.recent)
@@ -84,14 +85,14 @@ struct MsgContentView: View {
}
}
-func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text {
+func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false, showSecrets: Bool) -> Text {
let s = text
var res: Text
if let ft = formattedText, ft.count > 0 && ft.count <= 200 {
- res = formatText(ft[0], preview)
+ res = formatText(ft[0], preview, showSecret: showSecrets)
var i = 1
while i < ft.count {
- res = res + formatText(ft[i], preview)
+ res = res + formatText(ft[i], preview, showSecret: showSecrets)
i = i + 1
}
} else {
@@ -110,7 +111,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
}
}
-private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
+private func formatText(_ ft: FormattedText, _ preview: Bool, showSecret: Bool) -> Text {
let t = ft.text
if let f = ft.format {
switch (f) {
@@ -118,7 +119,13 @@ private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
case .italic: return Text(t).italic()
case .strikeThrough: return Text(t).strikethrough()
case .snippet: return Text(t).font(.body.monospaced())
- case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
+ case .secret: return
+ showSecret
+ ? Text(t)
+ : Text(AttributedString(t, attributes: AttributeContainer([
+ .foregroundColor: UIColor.clear as Any,
+ .backgroundColor: UIColor.secondarySystemFill as Any
+ ])))
case let .colored(color): return Text(t).foregroundColor(color.uiColor)
case .uri: return linkText(t, t, preview, prefix: "")
case let .simplexLink(linkType, simplexUri, smpHosts):
@@ -144,7 +151,7 @@ private func linkText(_ s: String, _ link: String, _ preview: Bool, prefix: Stri
]))).underline()
}
-private func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
+func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
linkType.description + " " + "(via \(smpHosts.first ?? "?"))"
}
@@ -156,7 +163,8 @@ struct MsgContentView_Previews: PreviewProvider {
text: chatItem.text,
formattedText: chatItem.formattedText,
sender: chatItem.memberDisplayName,
- meta: chatItem.meta
+ meta: chatItem.meta,
+ showSecrets: false
)
.environmentObject(Chat.sampleData)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
index 83c4cdcda6..8dd43cc01b 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
@@ -53,7 +53,9 @@ struct ChatItemInfoView: View {
}
private var title: String {
- ci.chatDir.sent
+ ci.localNote
+ ? NSLocalizedString("Saved message", comment: "message info title")
+ : ci.chatDir.sent
? NSLocalizedString("Sent message", comment: "message info title")
: NSLocalizedString("Received message", comment: "message info title")
}
@@ -110,7 +112,11 @@ struct ChatItemInfoView: View {
.bold()
.padding(.bottom)
- infoRow("Sent at", localTimestamp(meta.itemTs))
+ if ci.localNote {
+ infoRow("Created at", localTimestamp(meta.itemTs))
+ } else {
+ infoRow("Sent at", localTimestamp(meta.itemTs))
+ }
if !ci.chatDir.sent {
infoRow("Received at", localTimestamp(meta.createdAt))
}
@@ -168,7 +174,6 @@ struct ChatItemInfoView: View {
@ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
- .allowsHitTesting(false)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(ci, colorScheme))
@@ -198,7 +203,7 @@ struct ChatItemInfoView: View {
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil) -> some View {
if text != "" {
- messageText(text, formattedText, sender)
+ TextBubble(text: text, formattedText: formattedText, sender: sender)
} else {
Text("no text")
.italic()
@@ -206,6 +211,17 @@ struct ChatItemInfoView: View {
}
}
+ private struct TextBubble: View {
+ var text: String
+ var formattedText: [FormattedText]?
+ var sender: String? = nil
+ @State private var showSecrets = false
+
+ var body: some View {
+ toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, showSecrets: showSecrets))
+ }
+ }
+
@ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View {
GeometryReader { g in
let maxWidth = (g.size.width - 32) * 0.84
@@ -227,7 +243,6 @@ struct ChatItemInfoView: View {
@ViewBuilder private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
- .allowsHitTesting(false)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(quotedMsgFrameColor(qi, colorScheme))
@@ -341,7 +356,12 @@ struct ChatItemInfoView: View {
private func itemInfoShareText() -> String {
let meta = ci.meta
var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # "), title), ""]
- shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))]
+ shareText += [String.localizedStringWithFormat(
+ ci.localNote
+ ? NSLocalizedString("Created at: %@", comment: "copied message info")
+ : NSLocalizedString("Sent at: %@", comment: "copied message info"),
+ localTimestamp(meta.itemTs))
+ ]
if !ci.chatDir.sent {
shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))]
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift
index 657df60654..8f67a8f737 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift
@@ -109,6 +109,7 @@ struct ChatItemContentView: View {
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
case .sndModerated: deletedItemView()
case .rcvModerated: deletedItemView()
+ case .rcvBlocked: deletedItemView()
case let .invalidJSON(json): CIInvalidJSONView(json: json)
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift
index 6e2c0c1555..af53e7e476 100644
--- a/apps/ios/Shared/Views/Chat/ChatView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatView.swift
@@ -151,6 +151,8 @@ struct ChatView: View {
)
)
}
+ } else if case .local = cInfo {
+ ChatInfoToolbar(chat: chat)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -205,6 +207,8 @@ struct ChatView: View {
Image(systemName: "ellipsis")
}
}
+ case .local:
+ searchButton()
default:
EmptyView()
}
@@ -250,8 +254,8 @@ struct ChatView: View {
}
private func searchToolbar() -> some View {
- HStack {
- HStack {
+ HStack(spacing: 12) {
+ HStack(spacing: 4) {
Image(systemName: "magnifyingglass")
TextField("Search", text: $searchText)
.focused($searchFocussed)
@@ -264,9 +268,9 @@ struct ChatView: View {
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}
- .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
+ .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
.foregroundColor(.secondary)
- .background(Color(.secondarySystemBackground))
+ .background(Color(.tertiarySystemFill))
.cornerRadius(10.0)
Button ("Cancel") {
@@ -636,7 +640,7 @@ struct ChatView: View {
Button("Delete for me", role: .destructive) {
deleteMessage(.cidmInternal)
}
- if let di = deletingItem, di.meta.editable {
+ if let di = deletingItem, di.meta.editable && !di.localNote {
Button(broadcastDeleteButtonText, role: .destructive) {
deleteMessage(.cidmBroadcast)
}
@@ -720,7 +724,7 @@ struct ChatView: View {
}
menu.append(rm)
}
- if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
+ if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote {
menu.append(replyUIAction(ci))
}
let fileSource = getLoadedFileSource(ci.file)
@@ -748,9 +752,9 @@ struct ChatView: View {
if revealed {
menu.append(hideUIAction())
}
- if ci.meta.itemDeleted == nil,
+ if ci.meta.itemDeleted == nil && !ci.localNote,
let file = ci.file,
- let cancelAction = file.cancelAction {
+ let cancelAction = file.cancelAction {
menu.append(cancelFileUIAction(file.fileId, cancelAction))
}
if !live || !ci.meta.isLive {
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
index d089c7d6fe..b597926093 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
@@ -295,7 +295,7 @@ struct ComposeView: View {
sendMessage(ttl: ttl)
resetLinkPreview()
},
- sendLiveMessage: sendLiveMessage,
+ sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil,
updateLiveMessage: updateLiveMessage,
cancelLiveMessage: {
composeState.liveMessage = nil
@@ -689,7 +689,7 @@ struct ComposeView: View {
let file = voiceCryptoFile(recordingFileName)
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl)
case let .filePreview(_, file):
- if let savedFile = saveFileFromURL(file, encrypted: privacyEncryptLocalFilesGroupDefault.get()) {
+ if let savedFile = saveFileFromURL(file) {
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
}
@@ -792,15 +792,17 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
- if let chatItem = await apiSendMessage(
- type: chat.chatInfo.chatType,
- id: chat.chatInfo.apiId,
- file: file,
- quotedItemId: quoted,
- msg: mc,
- live: live,
- ttl: ttl
- ) {
+ if let chatItem = chat.chatInfo.chatType == .local
+ ? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc)
+ : await apiSendMessage(
+ type: chat.chatInfo.chatType,
+ id: chat.chatInfo.apiId,
+ file: file,
+ quotedItemId: quoted,
+ msg: mc,
+ live: live,
+ ttl: ttl
+ ) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift
index 868ae3274a..3eb128cded 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift
@@ -51,7 +51,8 @@ struct ContextItemView: View {
MsgContentView(
chat: chat,
text: contextItem.text,
- formattedText: contextItem.formattedText
+ formattedText: contextItem.formattedText,
+ showSecrets: false
)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)
diff --git a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift
index ff1892d996..57007fff3f 100644
--- a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift
+++ b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift
@@ -116,7 +116,6 @@ struct ContactPreferencesView: View {
private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View {
Text(feature.enabledDescription(enabled))
- .frame(height: 36, alignment: .topLeading)
}
private func savePreferences() {
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
index 09ead880ad..3879e78d3d 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
@@ -36,6 +36,8 @@ struct GroupChatInfoView: View {
case largeGroupReceiptsDisabled
case blockMemberAlert(mem: GroupMember)
case unblockMemberAlert(mem: GroupMember)
+ case blockForAllAlert(mem: GroupMember)
+ case unblockForAllAlert(mem: GroupMember)
case removeMemberAlert(mem: GroupMember)
case error(title: LocalizedStringKey, error: LocalizedStringKey)
@@ -48,6 +50,8 @@ struct GroupChatInfoView: View {
case .largeGroupReceiptsDisabled: return "largeGroupReceiptsDisabled"
case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)"
case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)"
+ case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)"
+ case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)"
case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)"
case let .error(title, _): return "error \(title)"
}
@@ -143,6 +147,8 @@ struct GroupChatInfoView: View {
case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert()
case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem)
case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem)
+ case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem)
+ case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem)
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
@@ -226,13 +232,10 @@ struct GroupChatInfoView: View {
.foregroundColor(.secondary)
}
Spacer()
- let role = member.memberRole
- if role == .owner || role == .admin {
- Text(member.memberRole.text)
- .foregroundColor(.secondary)
- }
+ memberInfo(member)
}
+ // revert from this:
if user {
v
} else if member.canBeRemoved(groupInfo: groupInfo) {
@@ -240,6 +243,43 @@ struct GroupChatInfoView: View {
} else {
blockSwipe(member, v)
}
+ // revert to this: vvv
+// if user {
+// v
+// } else if groupInfo.membership.memberRole >= .admin {
+// // TODO if there are more actions, refactor with lists of swipeActions
+// let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
+// let canRemove = member.canBeRemoved(groupInfo: groupInfo)
+// if canBlockForAll && canRemove {
+// removeSwipe(member, blockForAllSwipe(member, v))
+// } else if canBlockForAll {
+// blockForAllSwipe(member, v)
+// } else if canRemove {
+// removeSwipe(member, v)
+// } else {
+// v
+// }
+// } else {
+// if !member.blockedByAdmin {
+// blockSwipe(member, v)
+// } else {
+// v
+// }
+// }
+ // ^^^
+ }
+
+ @ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
+ if member.blocked {
+ Text("blocked")
+ .foregroundColor(.secondary)
+ } else {
+ let role = member.memberRole
+ if [.owner, .admin, .observer].contains(role) {
+ Text(member.memberRole.text)
+ .foregroundColor(.secondary)
+ }
+ }
}
private func blockSwipe(_ member: GroupMember, _ v: V) -> some View {
@@ -260,6 +300,24 @@ struct GroupChatInfoView: View {
}
}
+ private func blockForAllSwipe(_ member: GroupMember, _ v: V) -> some View {
+ v.swipeActions(edge: .leading) {
+ if member.blockedByAdmin {
+ Button {
+ alert = .unblockForAllAlert(mem: member)
+ } label: {
+ Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor)
+ }
+ } else {
+ Button {
+ alert = .blockForAllAlert(mem: member)
+ } label: {
+ Label("Block for all", systemImage: "hand.raised").foregroundColor(.secondary)
+ }
+ }
+ }
+ }
+
private func removeSwipe(_ member: GroupMember, _ v: V) -> some View {
v.swipeActions(edge: .trailing) {
Button(role: .destructive) {
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift
index bf2179bea4..c782e2a717 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift
@@ -18,6 +18,7 @@ struct GroupLinkView: View {
var linkCreatedCb: (() -> Void)? = nil
@State private var creatingLink = false
@State private var alert: GroupLinkAlert?
+ @State private var shouldCreate = true
private enum GroupLinkAlert: Identifiable {
case deleteLink
@@ -70,6 +71,7 @@ struct GroupLinkView: View {
}
.frame(height: 36)
SimpleXLinkQRCode(uri: groupLink)
+ .id("simplex-qrcode-view-for-\(groupLink)")
Button {
showShareSheet(items: [simplexChatLink(groupLink)])
} label: {
@@ -125,9 +127,10 @@ struct GroupLinkView: View {
}
}
.onAppear {
- if groupLink == nil && !creatingLink {
+ if groupLink == nil && !creatingLink && shouldCreate {
createGroupLink()
}
+ shouldCreate = false
}
}
}
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
index 7e336c3328..d2b0f77393 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
@@ -27,6 +27,8 @@ struct GroupMemberInfoView: View {
enum GroupMemberInfoViewAlert: Identifiable {
case blockMemberAlert(mem: GroupMember)
case unblockMemberAlert(mem: GroupMember)
+ case blockForAllAlert(mem: GroupMember)
+ case unblockForAllAlert(mem: GroupMember)
case removeMemberAlert(mem: GroupMember)
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert
@@ -39,6 +41,8 @@ struct GroupMemberInfoView: View {
switch self {
case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)"
case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)"
+ case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)"
+ case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)"
case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)"
case let .changeMemberRoleAlert(mem, role): return "changeMemberRoleAlert \(mem.groupMemberId) \(role.rawValue)"
case .switchAddressAlert: return "switchAddressAlert"
@@ -164,6 +168,7 @@ struct GroupMemberInfoView: View {
}
}
+ // revert from this:
Section {
if member.memberSettings.showMessages {
blockMemberButton(member)
@@ -171,9 +176,16 @@ struct GroupMemberInfoView: View {
unblockMemberButton(member)
}
if member.canBeRemoved(groupInfo: groupInfo) {
- removeMemberButton(member)
+ removeMemberButton(member)
}
}
+ // revert to this: vvv
+// if groupInfo.membership.memberRole >= .admin {
+// adminDestructiveSection(member)
+// } else {
+// nonAdminBlockSection(member)
+// }
+ // ^^^
if developerTools {
Section("For console") {
@@ -216,6 +228,8 @@ struct GroupMemberInfoView: View {
switch(alertItem) {
case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem)
case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem)
+ case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem)
+ case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem)
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
@@ -385,6 +399,55 @@ struct GroupMemberInfoView: View {
}
}
+ @ViewBuilder private func adminDestructiveSection(_ mem: GroupMember) -> some View {
+ let canBlockForAll = mem.canBlockForAll(groupInfo: groupInfo)
+ let canRemove = mem.canBeRemoved(groupInfo: groupInfo)
+ if canBlockForAll || canRemove {
+ Section {
+ if canBlockForAll {
+ if mem.blockedByAdmin {
+ unblockForAllButton(mem)
+ } else {
+ blockForAllButton(mem)
+ }
+ }
+ if canRemove {
+ removeMemberButton(mem)
+ }
+ }
+ }
+ }
+
+ private func nonAdminBlockSection(_ mem: GroupMember) -> some View {
+ Section {
+ if mem.blockedByAdmin {
+ Label("Blocked by admin", systemImage: "hand.raised")
+ .foregroundColor(.secondary)
+ } else if mem.memberSettings.showMessages {
+ blockMemberButton(mem)
+ } else {
+ unblockMemberButton(mem)
+ }
+ }
+ }
+
+ private func blockForAllButton(_ mem: GroupMember) -> some View {
+ Button(role: .destructive) {
+ alert = .blockForAllAlert(mem: mem)
+ } label: {
+ Label("Block for all", systemImage: "hand.raised")
+ .foregroundColor(.red)
+ }
+ }
+
+ private func unblockForAllButton(_ mem: GroupMember) -> some View {
+ Button {
+ alert = .unblockForAllAlert(mem: mem)
+ } label: {
+ Label("Unblock for all", systemImage: "hand.raised.slash")
+ }
+ }
+
private func blockMemberButton(_ mem: GroupMember) -> some View {
Button(role: .destructive) {
alert = .blockMemberAlert(mem: mem)
@@ -560,6 +623,41 @@ func updateMemberSettings(_ gInfo: GroupInfo, _ member: GroupMember, _ memberSet
}
}
+func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
+ Alert(
+ title: Text("Block member for all?"),
+ message: Text("All new messages from \(mem.chatViewName) will be hidden!"),
+ primaryButton: .destructive(Text("Block for all")) {
+ blockMemberForAll(gInfo, mem, true)
+ },
+ secondaryButton: .cancel()
+ )
+}
+
+func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
+ Alert(
+ title: Text("Unblock member for all?"),
+ message: Text("Messages from \(mem.chatViewName) will be shown!"),
+ primaryButton: .default(Text("Unblock for all")) {
+ blockMemberForAll(gInfo, mem, false)
+ },
+ secondaryButton: .cancel()
+ )
+}
+
+func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) {
+ Task {
+ do {
+ let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked)
+ await MainActor.run {
+ _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember)
+ }
+ } catch let error {
+ logger.error("apiBlockMemberForAll error: \(responseError(error))")
+ }
+ }
+}
+
struct GroupMemberInfoView_Previews: PreviewProvider {
static var previews: some View {
GroupMemberInfoView(
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
index 860a6febb0..d88bdfa4a4 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
@@ -28,6 +28,7 @@ struct GroupPreferencesView: View {
featureSection(.reactions, $preferences.reactions.enable)
featureSection(.voice, $preferences.voice.enable)
featureSection(.files, $preferences.files.enable)
+ featureSection(.history, $preferences.history.enable)
if groupInfo.canEdit {
Section {
@@ -96,7 +97,6 @@ struct GroupPreferencesView: View {
}
} footer: {
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
- .frame(height: 36, alignment: .topLeading)
}
}
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift
index 0e47d9dddf..e5ff644a91c 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift
@@ -53,8 +53,7 @@ struct GroupWelcomeView: View {
}
private func textPreview() -> some View {
- messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil)
- .allowsHitTesting(false)
+ messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
.frame(minHeight: 140, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift
index 7741512432..2435c9a4f5 100644
--- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift
@@ -11,7 +11,7 @@ import SwiftUI
struct ChatHelp: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
- @State private var showAddChat = false
+ @State private var newChatMenuOption: NewChatMenuOption? = nil
var body: some View {
ScrollView { chatHelp() }
@@ -39,13 +39,12 @@ struct ChatHelp: View {
HStack(spacing: 8) {
Text("Tap button ")
- NewChatButton(showAddChat: $showAddChat)
+ NewChatMenuButton(newChatMenuOption: $newChatMenuOption)
Text("above, then choose:")
}
- Text("**Create link / QR code** for your contact to use.")
- Text("**Paste received link** or open it in the browser and tap **Open in mobile app**.")
- Text("**Scan QR code**: to connect to your contact in person or via video call.")
+ Text("**Add contact**: to create a new invitation link, or connect via a link you received.")
+ Text("**Create group**: to create a new group.")
}
.padding(.top, 24)
diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
index 18464b3bb5..7fbc1e4ac8 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
@@ -44,6 +44,8 @@ struct ChatListNavLink: View {
contactNavLink(contact)
case let .group(groupInfo):
groupNavLink(groupInfo)
+ case let .local(noteFolder):
+ noteFolderNavLink(noteFolder)
case let .contactRequest(cReq):
contactRequestNavLink(cReq)
case let .contactConnection(cConn):
@@ -195,6 +197,24 @@ struct ChatListNavLink: View {
}
}
+ @ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View {
+ NavLinkPlain(
+ tag: chat.chatInfo.id,
+ selection: $chatModel.chatId,
+ label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) },
+ disabled: !noteFolder.ready
+ )
+ .frame(height: rowHeights[dynamicTypeSize])
+ .swipeActions(edge: .leading, allowsFullSwipe: true) {
+ markReadButton()
+ }
+ .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+ if !chat.chatItems.isEmpty {
+ clearNoteFolderButton()
+ }
+ }
+ }
+
private func joinGroupButton() -> some View {
Button {
inProgress = true
@@ -253,6 +273,15 @@ struct ChatListNavLink: View {
.tint(Color.orange)
}
+ private func clearNoteFolderButton() -> some View {
+ Button {
+ AlertManager.shared.showAlert(clearNoteFolderAlert())
+ } label: {
+ Label("Clear", systemImage: "gobackward")
+ }
+ .tint(Color.orange)
+ }
+
private func leaveGroupChatButton(_ groupInfo: GroupInfo) -> some View {
Button {
AlertManager.shared.showAlert(leaveGroupAlert(groupInfo))
@@ -357,6 +386,17 @@ struct ChatListNavLink: View {
)
}
+ private func clearNoteFolderAlert() -> Alert {
+ Alert(
+ title: Text("Clear private notes?"),
+ message: Text("All messages will be deleted - this cannot be undone!"),
+ primaryButton: .destructive(Text("Clear")) {
+ Task { await clearChat(chat) }
+ },
+ secondaryButton: .cancel()
+ )
+ }
+
private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert {
Alert(
title: Text("Leave group?"),
diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift
index 1d86733206..22807f6182 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift
@@ -12,8 +12,12 @@ import SimpleXChat
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
+ @State private var searchMode = false
+ @FocusState private var searchFocussed
@State private var searchText = ""
- @State private var showAddChat = false
+ @State private var searchShowingSimplexLink = false
+ @State private var searchChatFilteredBySimplexLink: String? = nil
+ @State private var newChatMenuOption: NewChatMenuOption? = nil
@State private var userPickerVisible = false
@State private var showConnectDesktop = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
@@ -62,11 +66,7 @@ struct ChatListView: View {
private var chatListView: some View {
VStack {
- if chatModel.chats.count > 0 {
- chatList.searchable(text: $searchText)
- } else {
- chatList
- }
+ chatList
}
.onDisappear() { withAnimation { userPickerVisible = false } }
.refreshable {
@@ -85,9 +85,9 @@ struct ChatListView: View {
secondaryButton: .cancel()
))
}
- .offset(x: -8)
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
+ .navigationBarHidden(searchMode)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
let user = chatModel.currentUser ?? User.sampleData
@@ -124,7 +124,7 @@ struct ChatListView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
switch chatModel.chatRunning {
- case .some(true): NewChatButton(showAddChat: $showAddChat)
+ case .some(true): NewChatMenuButton(newChatMenuOption: $newChatMenuOption)
case .some(false): chatStoppedIcon()
case .none: EmptyView()
}
@@ -144,11 +144,25 @@ struct ChatListView: View {
@ViewBuilder private var chatList: some View {
let cs = filteredChats()
ZStack {
- List {
- ForEach(cs, id: \.viewId) { chat in
- ChatListNavLink(chat: chat)
- .padding(.trailing, -16)
- .disabled(chatModel.chatRunning != true)
+ VStack {
+ List {
+ if !chatModel.chats.isEmpty {
+ ChatListSearchBar(
+ searchMode: $searchMode,
+ searchFocussed: $searchFocussed,
+ searchText: $searchText,
+ searchShowingSimplexLink: $searchShowingSimplexLink,
+ searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
+ )
+ .listRowSeparator(.hidden)
+ .frame(maxWidth: .infinity)
+ }
+ ForEach(cs, id: \.viewId) { chat in
+ ChatListNavLink(chat: chat)
+ .padding(.trailing, -16)
+ .disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
+ }
+ .offset(x: -8)
}
}
.onChange(of: chatModel.chatId) { _ in
@@ -182,7 +196,7 @@ struct ChatListView: View {
.padding(.trailing, 12)
connectButton("Tap to start a new chat") {
- showAddChat = true
+ newChatMenuOption = .newContact
}
Spacer()
@@ -214,22 +228,27 @@ struct ChatListView: View {
}
private func filteredChats() -> [Chat] {
- let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
- return s == "" && !showUnreadAndFavorites
+ if let linkChatId = searchChatFilteredBySimplexLink {
+ return chatModel.chats.filter { $0.id == linkChatId }
+ } else {
+ let s = searchString()
+ return s == "" && !showUnreadAndFavorites
? chatModel.chats
: chatModel.chats.filter { chat in
let cInfo = chat.chatInfo
switch cInfo {
case let .direct(contact):
return s == ""
- ? filtered(chat)
- : (viewNameContains(cInfo, s) ||
- contact.profile.displayName.localizedLowercase.contains(s) ||
- contact.fullName.localizedLowercase.contains(s))
+ ? filtered(chat)
+ : (viewNameContains(cInfo, s) ||
+ contact.profile.displayName.localizedLowercase.contains(s) ||
+ contact.fullName.localizedLowercase.contains(s))
case let .group(gInfo):
return s == ""
- ? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
- : viewNameContains(cInfo, s)
+ ? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
+ : viewNameContains(cInfo, s)
+ case .local:
+ return s == "" || viewNameContains(cInfo, s)
case .contactRequest:
return s == "" || viewNameContains(cInfo, s)
case let .contactConnection(conn):
@@ -238,6 +257,11 @@ struct ChatListView: View {
return false
}
}
+ }
+
+ func searchString() -> String {
+ searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
+ }
func filtered(_ chat: Chat) -> Bool {
(chat.chatInfo.chatSettings?.favorite ?? false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
@@ -249,6 +273,121 @@ struct ChatListView: View {
}
}
+struct ChatListSearchBar: View {
+ @EnvironmentObject var m: ChatModel
+ @Binding var searchMode: Bool
+ @FocusState.Binding var searchFocussed: Bool
+ @Binding var searchText: String
+ @Binding var searchShowingSimplexLink: Bool
+ @Binding var searchChatFilteredBySimplexLink: String?
+ @State private var ignoreSearchTextChange = false
+ @State private var showScanCodeSheet = false
+ @State private var alert: PlanAndConnectAlert?
+ @State private var sheet: PlanAndConnectActionSheet?
+
+ var body: some View {
+ VStack(spacing: 12) {
+ HStack(spacing: 12) {
+ HStack(spacing: 4) {
+ Image(systemName: "magnifyingglass")
+ TextField("Search or paste SimpleX link", text: $searchText)
+ .foregroundColor(searchShowingSimplexLink ? .secondary : .primary)
+ .disabled(searchShowingSimplexLink)
+ .focused($searchFocussed)
+ .frame(maxWidth: .infinity)
+ if !searchText.isEmpty {
+ Image(systemName: "xmark.circle.fill")
+ .onTapGesture {
+ searchText = ""
+ }
+ } else if !searchFocussed {
+ HStack(spacing: 24) {
+ if m.pasteboardHasStrings {
+ Image(systemName: "doc")
+ .onTapGesture {
+ if let str = UIPasteboard.general.string {
+ searchText = str
+ }
+ }
+ }
+
+ Image(systemName: "qrcode")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 20, height: 20)
+ .onTapGesture {
+ showScanCodeSheet = true
+ }
+ }
+ .padding(.trailing, 2)
+ }
+ }
+ .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
+ .foregroundColor(.secondary)
+ .background(Color(.tertiarySystemFill))
+ .cornerRadius(10.0)
+
+ if searchFocussed {
+ Text("Cancel")
+ .foregroundColor(.accentColor)
+ .onTapGesture {
+ searchText = ""
+ searchFocussed = false
+ }
+ }
+ }
+ Divider()
+ }
+ .sheet(isPresented: $showScanCodeSheet) {
+ NewChatView(selection: .connect, showQRCodeScanner: true)
+ .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) // fixes .refreshable in ChatListView affecting nested view
+ }
+ .onChange(of: searchFocussed) { sf in
+ withAnimation { searchMode = sf }
+ }
+ .onChange(of: searchText) { t in
+ if ignoreSearchTextChange {
+ ignoreSearchTextChange = false
+ } else {
+ if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue
+ searchFocussed = false
+ if case let .simplexLink(linkType, _, smpHosts) = link.format {
+ ignoreSearchTextChange = true
+ searchText = simplexLinkText(linkType, smpHosts)
+ }
+ searchShowingSimplexLink = true
+ searchChatFilteredBySimplexLink = nil
+ connect(link.text)
+ } else {
+ if t != "" { // if some other text is pasted, enter search mode
+ searchFocussed = true
+ }
+ searchShowingSimplexLink = false
+ searchChatFilteredBySimplexLink = nil
+ }
+ }
+ }
+ .alert(item: $alert) { a in
+ planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" })
+ }
+ .actionSheet(item: $sheet) { s in
+ planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" })
+ }
+ }
+
+ private func connect(_ link: String) {
+ planAndConnect(
+ link,
+ showAlert: { alert = $0 },
+ showActionSheet: { sheet = $0 },
+ dismiss: false,
+ incognito: nil,
+ filterKnownContact: { searchChatFilteredBySimplexLink = $0.id },
+ filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id }
+ )
+ }
+}
+
func chatStoppedIcon() -> some View {
Button {
AlertManager.shared.showAlertMsg(
diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
index 30068114f3..186a709ce8 100644
--- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
@@ -13,6 +13,7 @@ struct ChatPreviewView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var chat: Chat
@Binding var progressByTimeout: Bool
+ @State var deleting: Bool = false
@Environment(\.colorScheme) var colorScheme
var darkGreen = Color(red: 0, green: 0.5, blue: 0)
@@ -55,6 +56,9 @@ struct ChatPreviewView: View {
.frame(maxHeight: .infinity)
}
.padding(.bottom, -8)
+ .onChange(of: chatModel.deletedChats.contains(chat.chatInfo.id)) { contains in
+ deleting = contains
+ }
}
@ViewBuilder private func chatPreviewImageOverlayIcon() -> some View {
@@ -87,13 +91,13 @@ struct ChatPreviewView: View {
let t = Text(chat.chatInfo.chatViewName).font(.title3).fontWeight(.bold)
switch chat.chatInfo {
case let .direct(contact):
- previewTitle(contact.verified == true ? verifiedIcon + t : t)
+ previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil)
case let .group(groupInfo):
let v = previewTitle(t)
switch (groupInfo.membership.memberStatus) {
- case .memInvited: v.foregroundColor(chat.chatInfo.incognito ? .indigo : .accentColor)
+ case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor)
case .memAccepted: v.foregroundColor(.secondary)
- default: v
+ default: if deleting { v.foregroundColor(.secondary) } else { v }
}
default: previewTitle(t)
}
@@ -130,9 +134,9 @@ struct ChatPreviewView: View {
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
- .background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary)
+ .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary)
.cornerRadius(10)
- } else if !chat.chatInfo.ntfsEnabled {
+ } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local {
Image(systemName: "speaker.slash.fill")
.foregroundColor(.secondary)
} else if chat.chatInfo.chatSettings?.favorite ?? false {
@@ -150,7 +154,7 @@ struct ChatPreviewView: View {
let msg = draft.message
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
+ attachment()
- + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true)
+ + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false)
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
@@ -169,7 +173,7 @@ struct ChatPreviewView: View {
func chatItemPreview(_ cItem: ChatItem) -> Text {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
- return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true)
+ return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
func attachment() -> String? {
switch cItem.content.msgContent {
diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
index 6d2fba99c6..42e90232d6 100644
--- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
+++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
@@ -164,6 +164,28 @@ struct ContactConnectionInfo: View {
}
}
+private func shareLinkButton(_ connReqInvitation: String) -> some View {
+ Button {
+ showShareSheet(items: [simplexChatLink(connReqInvitation)])
+ } label: {
+ settingsRow("square.and.arrow.up") {
+ Text("Share 1-time link")
+ }
+ }
+}
+
+private func oneTimeLinkLearnMoreButton() -> some View {
+ NavigationLink {
+ AddContactLearnMore(showTitle: false)
+ .navigationTitle("One-time invitation link")
+ .navigationBarTitleDisplayMode(.large)
+ } label: {
+ settingsRow("info.circle") {
+ Text("Learn more")
+ }
+ }
+}
+
struct ContactConnectionInfo_Previews: PreviewProvider {
static var previews: some View {
ContactConnectionInfo(contactConnection: PendingContactConnection.getSampleData())
diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift
index 04e377f3a5..52ded44782 100644
--- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift
+++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift
@@ -149,7 +149,7 @@ struct DatabaseErrorView: View {
private func runChatSync(confirmMigrations: MigrationConfirmation? = nil) {
do {
resetChatCtrl()
- try initializeChat(start: m.v3DBMigration.startChat, dbKey: useKeychain ? nil : dbKey, confirmMigrations: confirmMigrations)
+ try initializeChat(start: m.v3DBMigration.startChat, confirmStart: m.v3DBMigration.startChat && AppChatState.shared.value == .stopped, dbKey: useKeychain ? nil : dbKey, confirmMigrations: confirmMigrations)
if let s = m.chatDbStatus {
status = s
let am = AlertManager.shared
diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift
index 72515a1fac..31b1f618e3 100644
--- a/apps/ios/Shared/Views/Database/DatabaseView.swift
+++ b/apps/ios/Shared/Views/Database/DatabaseView.swift
@@ -484,6 +484,7 @@ func deleteChatAsync() async throws {
try await apiDeleteStorage()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
+ deleteAppDatabaseAndFiles()
}
struct DatabaseView_Previews: PreviewProvider {
diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift
index 1b344148c0..e253cdd72c 100644
--- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift
+++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift
@@ -10,6 +10,7 @@ import SwiftUI
import SimpleXChat
struct ChatInfoImage: View {
+ @Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat
var color = Color(uiColor: .tertiarySystemGroupedBackground)
@@ -18,13 +19,16 @@ struct ChatInfoImage: View {
switch chat.chatInfo {
case .direct: iconName = "person.crop.circle.fill"
case .group: iconName = "person.2.circle.fill"
+ case .local: iconName = "folder.circle.fill"
case .contactRequest: iconName = "person.crop.circle.fill"
default: iconName = "circle.fill"
}
+ let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark
+ let iconColor = if case .local = chat.chatInfo { notesColor } else { color }
return ProfileImage(
imageStr: chat.chatInfo.image,
iconName: iconName,
- color: color
+ color: iconColor
)
}
}
diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
index bdb5b03e8c..9691a9efd3 100644
--- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
+++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
@@ -13,19 +13,28 @@ struct LocalAuthView: View {
@EnvironmentObject var m: ChatModel
var authRequest: LocalAuthRequest
@State private var password = ""
+ @State private var allowToReact = true
var body: some View {
- PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") {
+ PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit",
+ buttonsEnabled: $allowToReact) {
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
+ allowToReact = false
deleteStorageAndRestart(sdPassword) { r in
m.laRequest = nil
authRequest.completed(r)
}
return
}
- let r: LAResult = password == authRequest.password
- ? .success
- : .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
+ let r: LAResult
+ if password == authRequest.password {
+ if authRequest.selfDestruct && kcSelfDestructPassword.get() != nil && !m.chatInitialized {
+ initChatAndMigrate()
+ }
+ r = .success
+ } else {
+ r = .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
+ }
m.laRequest = nil
authRequest.completed(r)
} cancel: {
@@ -37,8 +46,27 @@ struct LocalAuthView: View {
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
Task {
do {
- try await stopChatAsync()
- try await deleteChatAsync()
+ /** Waiting until [initializeChat] finishes */
+ while (m.ctrlInitInProgress) {
+ try await Task.sleep(nanoseconds: 50_000000)
+ }
+ if m.chatRunning == true {
+ try await stopChatAsync()
+ }
+ if m.chatInitialized {
+ /**
+ * The following sequence can bring a user here:
+ * the user opened the app, entered app passcode, went to background, returned back, entered self-destruct code.
+ * In this case database should be closed to prevent possible situation when OS can deny database removal command
+ * */
+ chatCloseStore()
+ }
+ deleteAppDatabaseAndFiles()
+ // Clear sensitive data on screen just in case app fails to hide its views while new database is created
+ m.chatId = nil
+ m.reversedChatItems = []
+ m.chats = []
+ m.users = []
_ = kcAppPassword.set(password)
_ = kcSelfDestructPassword.remove()
await NtfManager.shared.removeAllNotifications()
@@ -53,7 +81,7 @@ struct LocalAuthView: View {
try initializeChat(start: true)
m.chatDbChanged = false
AppChatState.shared.set(.active)
- if m.currentUser != nil { return }
+ if m.currentUser != nil || !m.chatInitialized { return }
var profile: Profile? = nil
if let displayName = displayName, displayName != "" {
profile = Profile(displayName: displayName, fullName: "")
diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift
index c73ded2d28..9e0d7f38b5 100644
--- a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift
+++ b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift
@@ -14,6 +14,8 @@ struct PasscodeView: View {
var reason: String? = nil
var submitLabel: LocalizedStringKey
var submitEnabled: ((String) -> Bool)?
+ @Binding var buttonsEnabled: Bool
+
var submit: () -> Void
var cancel: () -> Void
@@ -70,11 +72,11 @@ struct PasscodeView: View {
@ViewBuilder private func buttonsView() -> some View {
Button(action: cancel) {
Label("Cancel", systemImage: "multiply")
- }
+ }.disabled(!buttonsEnabled)
Button(action: submit) {
Label(submitLabel, systemImage: "checkmark")
}
- .disabled(submitEnabled?(passcode) == false || passcode.count < 4)
+ .disabled(submitEnabled?(passcode) == false || passcode.count < 4 || !buttonsEnabled)
}
}
@@ -85,6 +87,7 @@ struct PasscodeViewView_Previews: PreviewProvider {
title: "Enter Passcode",
reason: "Unlock app",
submitLabel: "Submit",
+ buttonsEnabled: Binding.constant(true),
submit: {},
cancel: {}
)
diff --git a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift
index 76cd3e279a..7ec3ee1a42 100644
--- a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift
+++ b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift
@@ -11,6 +11,7 @@ import SimpleXChat
struct SetAppPasscodeView: View {
var passcodeKeychain: KeyChainItem = kcAppPassword
+ var prohibitedPasscodeKeychain: KeyChainItem = kcSelfDestructPassword
var title: LocalizedStringKey = "New Passcode"
var reason: String?
var submit: () -> Void
@@ -41,7 +42,10 @@ struct SetAppPasscodeView: View {
}
}
} else {
- setPasswordView(title: title, submitLabel: "Save") {
+ setPasswordView(title: title,
+ submitLabel: "Save",
+ // Do not allow to set app passcode == selfDestruct passcode
+ submitEnabled: { pwd in pwd != prohibitedPasscodeKeychain.get() }) {
enteredPassword = passcode
passcode = ""
confirming = true
@@ -54,7 +58,7 @@ struct SetAppPasscodeView: View {
}
private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View {
- PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) {
+ PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, buttonsEnabled: Binding.constant(true), submit: submit) {
dismiss()
cancel()
}
diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
index 182149cbde..45eb783326 100644
--- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
+++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
@@ -9,8 +9,20 @@
import SwiftUI
struct AddContactLearnMore: View {
+ var showTitle: Bool
+
var body: some View {
List {
+ if showTitle {
+ Text("One-time invitation link")
+ .font(.largeTitle)
+ .bold()
+ .fixedSize(horizontal: false, vertical: true)
+ .padding(.vertical)
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ }
VStack(alignment: .leading, spacing: 18) {
Text("To connect, your contact can scan QR code or use the link in the app.")
Text("If you can't meet in person, show QR code in a video call, or share the link.")
@@ -23,6 +35,6 @@ struct AddContactLearnMore: View {
struct AddContactLearnMore_Previews: PreviewProvider {
static var previews: some View {
- AddContactLearnMore()
+ AddContactLearnMore(showTitle: true)
}
}
diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift
deleted file mode 100644
index de8e35d2a6..0000000000
--- a/apps/ios/Shared/Views/NewChat/AddContactView.swift
+++ /dev/null
@@ -1,129 +0,0 @@
-//
-// AddContactView.swift
-// SimpleX
-//
-// Created by Evgeny Poberezkin on 29/01/2022.
-// Copyright © 2022 SimpleX Chat. All rights reserved.
-//
-
-import SwiftUI
-import CoreImage.CIFilterBuiltins
-import SimpleXChat
-
-struct AddContactView: View {
- @EnvironmentObject private var chatModel: ChatModel
- @Binding var contactConnection: PendingContactConnection?
- var connReqInvitation: String
- @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
-
- var body: some View {
- VStack {
- List {
- Section {
- if connReqInvitation != "" {
- SimpleXLinkQRCode(uri: connReqInvitation)
- } else {
- ProgressView()
- .progressViewStyle(.circular)
- .scaleEffect(2)
- .frame(maxWidth: .infinity)
- .padding(.vertical)
- }
- IncognitoToggle(incognitoEnabled: $incognitoDefault)
- .disabled(contactConnection == nil)
- shareLinkButton(connReqInvitation)
- oneTimeLinkLearnMoreButton()
- } header: {
- Text("1-time link")
- } footer: {
- sharedProfileInfo(incognitoDefault)
- }
- }
- }
- .onAppear { chatModel.connReqInv = connReqInvitation }
- .onChange(of: incognitoDefault) { incognito in
- Task {
- do {
- if let contactConn = contactConnection,
- let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
- await MainActor.run {
- contactConnection = conn
- chatModel.updateContactConnection(conn)
- }
- }
- } catch {
- logger.error("apiSetConnectionIncognito error: \(responseError(error))")
- }
- }
- }
- }
-}
-
-struct IncognitoToggle: View {
- @Binding var incognitoEnabled: Bool
- @State private var showIncognitoSheet = false
-
- var body: some View {
- ZStack(alignment: .leading) {
- Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks")
- .frame(maxWidth: 24, maxHeight: 24, alignment: .center)
- .foregroundColor(incognitoEnabled ? Color.indigo : .secondary)
- .font(.system(size: 14))
- Toggle(isOn: $incognitoEnabled) {
- HStack(spacing: 6) {
- Text("Incognito")
- Image(systemName: "info.circle")
- .foregroundColor(.accentColor)
- .font(.system(size: 14))
- }
- .onTapGesture {
- showIncognitoSheet = true
- }
- }
- .padding(.leading, 36)
- }
- .sheet(isPresented: $showIncognitoSheet) {
- IncognitoHelp()
- }
- }
-}
-
-func sharedProfileInfo(_ incognito: Bool) -> Text {
- let name = ChatModel.shared.currentUser?.displayName ?? ""
- return Text(
- incognito
- ? "A new random profile will be shared."
- : "Your profile **\(name)** will be shared."
- )
-}
-
-func shareLinkButton(_ connReqInvitation: String) -> some View {
- Button {
- showShareSheet(items: [simplexChatLink(connReqInvitation)])
- } label: {
- settingsRow("square.and.arrow.up") {
- Text("Share 1-time link")
- }
- }
-}
-
-func oneTimeLinkLearnMoreButton() -> some View {
- NavigationLink {
- AddContactLearnMore()
- .navigationTitle("One-time invitation link")
- .navigationBarTitleDisplayMode(.large)
- } label: {
- settingsRow("info.circle") {
- Text("Learn more")
- }
- }
-}
-
-struct AddContactView_Previews: PreviewProvider {
- static var previews: some View {
- AddContactView(
- contactConnection: Binding.constant(PendingContactConnection.getSampleData()),
- connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D"
- )
- }
-}
diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
index 6c7919669b..3f3623033e 100644
--- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift
+++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
@@ -187,6 +187,7 @@ struct AddGroupView: View {
hideKeyboard()
do {
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
+ profile.groupPreferences = GroupPreferences(history: GroupPreference(enable: .on))
let gInfo = try apiNewGroup(incognito: incognitoDefault, groupProfile: profile)
Task {
let groupMembers = await apiListMembers(gInfo.groupId)
diff --git a/apps/ios/Shared/Views/NewChat/ConnectViaLinkView.swift b/apps/ios/Shared/Views/NewChat/ConnectViaLinkView.swift
deleted file mode 100644
index 9df767485e..0000000000
--- a/apps/ios/Shared/Views/NewChat/ConnectViaLinkView.swift
+++ /dev/null
@@ -1,42 +0,0 @@
-//
-// ConnectViaLinkView.swift
-// SimpleX (iOS)
-//
-// Created by Evgeny on 21/09/2022.
-// Copyright © 2022 SimpleX Chat. All rights reserved.
-//
-
-import SwiftUI
-
-enum ConnectViaLinkTab: String {
- case scan
- case paste
-}
-
-struct ConnectViaLinkView: View {
- @State private var selection: ConnectViaLinkTab = connectViaLinkTabDefault.get()
-
- var body: some View {
- TabView(selection: $selection) {
- ScanToConnectView()
- .tabItem {
- Label("Scan QR code", systemImage: "qrcode")
- }
- .tag(ConnectViaLinkTab.scan)
- PasteToConnectView()
- .tabItem {
- Label("Paste received link", systemImage: "doc.plaintext")
- }
- .tag(ConnectViaLinkTab.paste)
- }
- .onChange(of: selection) { _ in
- connectViaLinkTabDefault.set(selection)
- }
- }
-}
-
-struct ConnectViaLinkView_Previews: PreviewProvider {
- static var previews: some View {
- ConnectViaLinkView()
- }
-}
diff --git a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift
deleted file mode 100644
index 3be9e1c3b3..0000000000
--- a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift
+++ /dev/null
@@ -1,94 +0,0 @@
-//
-// CreateLinkView.swift
-// SimpleX (iOS)
-//
-// Created by Evgeny on 21/09/2022.
-// Copyright © 2022 SimpleX Chat. All rights reserved.
-//
-
-import SwiftUI
-import SimpleXChat
-
-enum CreateLinkTab {
- case oneTime
- case longTerm
-
- var title: LocalizedStringKey {
- switch self {
- case .oneTime: return "One-time invitation link"
- case .longTerm: return "Your SimpleX address"
- }
- }
-}
-
-struct CreateLinkView: View {
- @EnvironmentObject var m: ChatModel
- @State var selection: CreateLinkTab
- @State var connReqInvitation: String = ""
- @State var contactConnection: PendingContactConnection? = nil
- @State private var creatingConnReq = false
- var viaNavLink = false
-
- var body: some View {
- if viaNavLink {
- createLinkView()
- } else {
- NavigationView {
- createLinkView()
- }
- }
- }
-
- private func createLinkView() -> some View {
- TabView(selection: $selection) {
- AddContactView(contactConnection: $contactConnection, connReqInvitation: connReqInvitation)
- .tabItem {
- Label(
- connReqInvitation == ""
- ? "Create one-time invitation link"
- : "One-time invitation link",
- systemImage: "1.circle"
- )
- }
- .tag(CreateLinkTab.oneTime)
- UserAddressView(viaCreateLinkView: true)
- .tabItem {
- Label("Your SimpleX address", systemImage: "infinity.circle")
- }
- .tag(CreateLinkTab.longTerm)
- }
- .onChange(of: selection) { _ in
- if case .oneTime = selection, connReqInvitation == "", contactConnection == nil && !creatingConnReq {
- createInvitation()
- }
- }
- .onAppear { m.connReqInv = connReqInvitation }
- .onDisappear { m.connReqInv = nil }
- .navigationTitle(selection.title)
- .navigationBarTitleDisplayMode(.large)
- }
-
- private func createInvitation() {
- creatingConnReq = true
- Task {
- if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
- await MainActor.run {
- m.updateContactConnection(pcc)
- connReqInvitation = connReq
- contactConnection = pcc
- m.connReqInv = connReq
- }
- } else {
- await MainActor.run {
- creatingConnReq = false
- }
- }
- }
- }
-}
-
-struct CreateLinkView_Previews: PreviewProvider {
- static var previews: some View {
- CreateLinkView(selection: CreateLinkTab.oneTime)
- }
-}
diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift
deleted file mode 100644
index 170805b488..0000000000
--- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift
+++ /dev/null
@@ -1,466 +0,0 @@
-//
-// NewChatButton.swift
-// SimpleX
-//
-// Created by Evgeny Poberezkin on 31/01/2022.
-// Copyright © 2022 SimpleX Chat. All rights reserved.
-//
-
-import SwiftUI
-import SimpleXChat
-
-enum NewChatAction: Identifiable {
- case createLink(link: String, connection: PendingContactConnection)
- case connectViaLink
- case createGroup
-
- var id: String {
- switch self {
- case let .createLink(link, _): return "createLink \(link)"
- case .connectViaLink: return "connectViaLink"
- case .createGroup: return "createGroup"
- }
- }
-}
-
-struct NewChatButton: View {
- @Binding var showAddChat: Bool
- @State private var actionSheet: NewChatAction?
-
- var body: some View {
- Button { showAddChat = true } label: {
- Image(systemName: "square.and.pencil")
- .resizable()
- .scaledToFit()
- .frame(width: 24, height: 24)
- }
- .confirmationDialog("Start a new chat", isPresented: $showAddChat, titleVisibility: .visible) {
- Button("Share one-time invitation link") { addContactAction() }
- Button("Connect via link / QR code") { actionSheet = .connectViaLink }
- Button("Create secret group") { actionSheet = .createGroup }
- }
- .sheet(item: $actionSheet) { sheet in
- switch sheet {
- case let .createLink(link, pcc):
- CreateLinkView(selection: .oneTime, connReqInvitation: link, contactConnection: pcc)
- case .connectViaLink: ConnectViaLinkView()
- case .createGroup: AddGroupView()
- }
- }
- }
-
- func addContactAction() {
- Task {
- if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
- await MainActor.run {
- ChatModel.shared.updateContactConnection(pcc)
- }
- actionSheet = .createLink(link: connReq, connection: pcc)
- }
- }
- }
-}
-
-enum PlanAndConnectAlert: Identifiable {
- case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
- case invitationLinkConnecting(connectionLink: String)
- case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
- case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
- case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
- case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
- case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
-
- var id: String {
- switch self {
- case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
- case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
- case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
- case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)"
- case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
- case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)"
- case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
- }
- }
-}
-
-func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
- switch alert {
- case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
- return Alert(
- title: Text("Connect to yourself?"),
- message: Text("This is your own one-time link!"),
- primaryButton: .destructive(
- Text(incognito ? "Connect incognito" : "Connect"),
- action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
- ),
- secondaryButton: .cancel()
- )
- case .invitationLinkConnecting:
- return Alert(
- title: Text("Already connecting!"),
- message: Text("You are already connecting via this one-time link!")
- )
- case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito):
- return Alert(
- title: Text("Connect to yourself?"),
- message: Text("This is your own SimpleX address!"),
- primaryButton: .destructive(
- Text(incognito ? "Connect incognito" : "Connect"),
- action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
- ),
- secondaryButton: .cancel()
- )
- case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
- return Alert(
- title: Text("Repeat connection request?"),
- message: Text("You have already requested connection via this address!"),
- primaryButton: .destructive(
- Text(incognito ? "Connect incognito" : "Connect"),
- action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
- ),
- secondaryButton: .cancel()
- )
- case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
- return Alert(
- title: Text("Join group?"),
- message: Text("You will connect to all group members."),
- primaryButton: .default(
- Text(incognito ? "Join incognito" : "Join"),
- action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
- ),
- secondaryButton: .cancel()
- )
- case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
- return Alert(
- title: Text("Repeat join request?"),
- message: Text("You are already joining the group via this link!"),
- primaryButton: .destructive(
- Text(incognito ? "Join incognito" : "Join"),
- action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
- ),
- secondaryButton: .cancel()
- )
- case let .groupLinkConnecting(_, groupInfo):
- if let groupInfo = groupInfo {
- return Alert(
- title: Text("Group already exists!"),
- message: Text("You are already joining the group \(groupInfo.displayName).")
- )
- } else {
- return Alert(
- title: Text("Already joining the group!"),
- message: Text("You are already joining the group via this link.")
- )
- }
- }
-}
-
-enum PlanAndConnectActionSheet: Identifiable {
- case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
- case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
- case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact)
- case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
-
- var id: String {
- switch self {
- case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
- case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)"
- case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)"
- case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
- }
- }
-}
-
-func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool) -> ActionSheet {
- switch sheet {
- case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
- return ActionSheet(
- title: Text(title),
- buttons: [
- .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
- .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
- .cancel()
- ]
- )
- case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title):
- return ActionSheet(
- title: Text(title),
- buttons: [
- .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
- .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
- .cancel()
- ]
- )
- case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact):
- return ActionSheet(
- title: Text("Connect with \(contact.chatViewName)"),
- buttons: [
- .default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false) },
- .default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true) },
- .cancel()
- ]
- )
- case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo):
- if let incognito = incognito {
- return ActionSheet(
- title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
- buttons: [
- .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
- .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) },
- .cancel()
- ]
- )
- } else {
- return ActionSheet(
- title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
- buttons: [
- .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
- .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
- .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
- .cancel()
- ]
- )
- }
- }
-}
-
-func planAndConnect(
- _ connectionLink: String,
- showAlert: @escaping (PlanAndConnectAlert) -> Void,
- showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
- dismiss: Bool,
- incognito: Bool?
-) {
- Task {
- do {
- let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
- switch connectionPlan {
- case let .invitationLink(ilp):
- switch ilp {
- case .ok:
- logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")")
- if let incognito = incognito {
- connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
- } else {
- showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
- }
- case .ownLink:
- logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
- if let incognito = incognito {
- showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
- } else {
- showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
- }
- case let .connecting(contact_):
- logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
- if let contact = contact_ {
- openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
- } else {
- showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
- }
- case let .known(contact):
- logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
- openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
- }
- case let .contactAddress(cap):
- switch cap {
- case .ok:
- logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")")
- if let incognito = incognito {
- connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
- } else {
- showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
- }
- case .ownLink:
- logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
- if let incognito = incognito {
- showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
- } else {
- showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
- }
- case .connectingConfirmReconnect:
- logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
- if let incognito = incognito {
- showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
- } else {
- showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
- }
- case let .connectingProhibit(contact):
- logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
- openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
- case let .known(contact):
- logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
- openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
- case let .contactViaAddress(contact):
- logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")")
- if let incognito = incognito {
- connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito)
- } else {
- showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
- }
- }
- case let .groupLink(glp):
- switch glp {
- case .ok:
- if let incognito = incognito {
- showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
- } else {
- showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
- }
- case let .ownLink(groupInfo):
- logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
- showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
- case .connectingConfirmReconnect:
- logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
- if let incognito = incognito {
- showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
- } else {
- showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
- }
- case let .connectingProhibit(groupInfo_):
- logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
- showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
- case let .known(groupInfo):
- logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
- openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
- }
- }
- } catch {
- logger.debug("planAndConnect, plan error")
- if let incognito = incognito {
- connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito)
- } else {
- showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
- }
- }
- }
-}
-
-private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incognito: Bool) {
- Task {
- if dismiss {
- DispatchQueue.main.async {
- dismissAllSheets(animated: true)
- }
- }
- _ = await connectContactViaAddress(contact.contactId, incognito)
- }
-}
-
-private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
- Task {
- if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) {
- await MainActor.run {
- ChatModel.shared.updateContactConnection(pcc)
- }
- let crt: ConnReqType
- if let plan = connectionPlan {
- crt = planToConnReqType(plan)
- } else {
- crt = connReqType
- }
- DispatchQueue.main.async {
- if dismiss {
- dismissAllSheets(animated: true) {
- AlertManager.shared.showAlert(connReqSentAlert(crt))
- }
- } else {
- AlertManager.shared.showAlert(connReqSentAlert(crt))
- }
- }
- } else {
- if dismiss {
- DispatchQueue.main.async {
- dismissAllSheets(animated: true)
- }
- }
- }
- }
-}
-
-func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
- Task {
- let m = ChatModel.shared
- if let c = m.getContactChat(contact.contactId) {
- DispatchQueue.main.async {
- if dismiss {
- dismissAllSheets(animated: true) {
- m.chatId = c.id
- showAlreadyExistsAlert?()
- }
- } else {
- m.chatId = c.id
- showAlreadyExistsAlert?()
- }
- }
- }
- }
-}
-
-func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
- Task {
- let m = ChatModel.shared
- if let g = m.getGroupChat(groupInfo.groupId) {
- DispatchQueue.main.async {
- if dismiss {
- dismissAllSheets(animated: true) {
- m.chatId = g.id
- showAlreadyExistsAlert?()
- }
- } else {
- m.chatId = g.id
- showAlreadyExistsAlert?()
- }
- }
- }
- }
-}
-
-func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
- mkAlert(
- title: "Contact already exists",
- message: "You are already connecting to \(contact.displayName)."
- )
-}
-
-func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
- mkAlert(
- title: "Group already exists",
- message: "You are already in group \(groupInfo.displayName)."
- )
-}
-
-enum ConnReqType: Equatable {
- case invitation
- case contact
- case groupLink
-
- var connReqSentText: LocalizedStringKey {
- switch self {
- case .invitation: return "You will be connected when your contact's device is online, please wait or check later!"
- case .contact: return "You will be connected when your connection request is accepted, please wait or check later!"
- case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!"
- }
- }
-}
-
-private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
- switch connectionPlan {
- case .invitationLink: return .invitation
- case .contactAddress: return .contact
- case .groupLink: return .groupLink
- }
-}
-
-func connReqSentAlert(_ type: ConnReqType) -> Alert {
- return mkAlert(
- title: "Connection request sent!",
- message: type.connReqSentText
- )
-}
-
-struct NewChatButton_Previews: PreviewProvider {
- static var previews: some View {
- NewChatButton(showAddChat: Binding.constant(false))
- }
-}
diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift
new file mode 100644
index 0000000000..c3452ce18d
--- /dev/null
+++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift
@@ -0,0 +1,52 @@
+//
+// NewChatMenuButton.swift
+// SimpleX (iOS)
+//
+// Created by spaced4ndy on 28.11.2023.
+// Copyright © 2023 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+
+enum NewChatMenuOption: Identifiable {
+ case newContact
+ case newGroup
+
+ var id: Self { self }
+}
+
+struct NewChatMenuButton: View {
+ @Binding var newChatMenuOption: NewChatMenuOption?
+
+ var body: some View {
+ Menu {
+ Button {
+ newChatMenuOption = .newContact
+ } label: {
+ Text("Add contact")
+ }
+ Button {
+ newChatMenuOption = .newGroup
+ } label: {
+ Text("Create group")
+ }
+ } label: {
+ Image(systemName: "square.and.pencil")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 24, height: 24)
+ }
+ .sheet(item: $newChatMenuOption) { opt in
+ switch opt {
+ case .newContact: NewChatView(selection: .invite)
+ case .newGroup: AddGroupView()
+ }
+ }
+ }
+}
+
+#Preview {
+ NewChatMenuButton(
+ newChatMenuOption: Binding.constant(nil)
+ )
+}
diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift
new file mode 100644
index 0000000000..b78d92ffc8
--- /dev/null
+++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift
@@ -0,0 +1,959 @@
+//
+// NewChatView.swift
+// SimpleX (iOS)
+//
+// Created by spaced4ndy on 28.11.2023.
+// Copyright © 2023 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+import SimpleXChat
+import CodeScanner
+import AVFoundation
+
+enum SomeAlert: Identifiable {
+ case someAlert(alert: Alert, id: String)
+
+ var id: String {
+ switch self {
+ case let .someAlert(_, id): return id
+ }
+ }
+}
+
+private enum NewChatViewAlert: Identifiable {
+ case planAndConnectAlert(alert: PlanAndConnectAlert)
+ case newChatSomeAlert(alert: SomeAlert)
+
+ var id: String {
+ switch self {
+ case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)"
+ case let .newChatSomeAlert(alert): return "newChatSomeAlert \(alert.id)"
+ }
+ }
+}
+
+enum NewChatOption: Identifiable {
+ case invite
+ case connect
+
+ var id: Self { self }
+}
+
+struct NewChatView: View {
+ @EnvironmentObject var m: ChatModel
+ @State var selection: NewChatOption
+ @State var showQRCodeScanner = false
+ @State private var invitationUsed: Bool = false
+ @State private var contactConnection: PendingContactConnection? = nil
+ @State private var connReqInvitation: String = ""
+ @State private var creatingConnReq = false
+ @State private var pastedLink: String = ""
+ @State private var alert: NewChatViewAlert?
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ HStack {
+ Text("New chat")
+ .font(.largeTitle)
+ .bold()
+ .fixedSize(horizontal: false, vertical: true)
+ Spacer()
+ InfoSheetButton {
+ AddContactLearnMore(showTitle: true)
+ }
+ }
+ .padding()
+ .padding(.top)
+
+ Picker("New chat", selection: $selection) {
+ Label("Add contact", systemImage: "link")
+ .tag(NewChatOption.invite)
+ Label("Connect via link", systemImage: "qrcode")
+ .tag(NewChatOption.connect)
+ }
+ .pickerStyle(.segmented)
+ .padding()
+
+ VStack {
+ // it seems there's a bug in iOS 15 if several views in switch (or if-else) statement have different transitions
+ // https://developer.apple.com/forums/thread/714977?answerId=731615022#731615022
+ if case .invite = selection {
+ prepareAndInviteView()
+ .transition(.move(edge: .leading))
+ .onAppear {
+ createInvitation()
+ }
+ }
+ if case .connect = selection {
+ ConnectView(showQRCodeScanner: showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
+ .transition(.move(edge: .trailing))
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(
+ // Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton)
+ Rectangle()
+ .fill(Color(uiColor: .systemGroupedBackground))
+ )
+ .animation(.easeInOut(duration: 0.3333), value: selection)
+ .gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local)
+ .onChanged { value in
+ switch(value.translation.width, value.translation.height) {
+ case (...0, -30...30): // left swipe
+ if selection == .invite {
+ selection = .connect
+ }
+ case (0..., -30...30): // right swipe
+ if selection == .connect {
+ selection = .invite
+ }
+ default: ()
+ }
+ }
+ )
+ }
+ .background(Color(.systemGroupedBackground))
+ .onChange(of: invitationUsed) { used in
+ if used && !(m.showingInvitation?.connChatUsed ?? true) {
+ m.markShowingInvitationUsed()
+ }
+ }
+ .onDisappear {
+ if !(m.showingInvitation?.connChatUsed ?? true),
+ let conn = contactConnection {
+ AlertManager.shared.showAlert(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: []
+ ))
+ }
+ }
+ ))
+ }
+ m.showingInvitation = nil
+ }
+ .alert(item: $alert) { a in
+ switch(a) {
+ case let .planAndConnectAlert(alert):
+ return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
+ case let .newChatSomeAlert(.someAlert(alert, _)):
+ return alert
+ }
+ }
+ }
+
+ private func prepareAndInviteView() -> some View {
+ ZStack { // ZStack is needed for views to not make transitions between each other
+ if connReqInvitation != "" {
+ InviteView(
+ invitationUsed: $invitationUsed,
+ contactConnection: $contactConnection,
+ connReqInvitation: connReqInvitation
+ )
+ } else if creatingConnReq {
+ creatingLinkProgressView()
+ } else {
+ retryButton()
+ }
+ }
+ }
+
+ private func createInvitation() {
+ if connReqInvitation == "" && contactConnection == nil && !creatingConnReq {
+ creatingConnReq = true
+ Task {
+ _ = try? await Task.sleep(nanoseconds: 250_000000)
+ let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get())
+ if let (connReq, pcc) = r {
+ await MainActor.run {
+ m.updateContactConnection(pcc)
+ m.showingInvitation = ShowingInvitation(connId: pcc.id, connChatUsed: false)
+ connReqInvitation = connReq
+ contactConnection = pcc
+ }
+ } else {
+ await MainActor.run {
+ creatingConnReq = false
+ if let apiAlert = apiAlert {
+ alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error"))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Rectangle here and in retryButton are needed for gesture to work
+ private func creatingLinkProgressView() -> some View {
+ ProgressView("Creating link…")
+ .progressViewStyle(.circular)
+ }
+
+ private func retryButton() -> some View {
+ Button(action: createInvitation) {
+ VStack(spacing: 6) {
+ Image(systemName: "arrow.counterclockwise")
+ Text("Retry")
+ }
+ }
+ }
+}
+
+private struct InviteView: View {
+ @EnvironmentObject var chatModel: ChatModel
+ @Binding var invitationUsed: Bool
+ @Binding var contactConnection: PendingContactConnection?
+ var connReqInvitation: String
+ @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
+
+ var body: some View {
+ List {
+ Section("Share this 1-time invite link") {
+ shareLinkView()
+ }
+ .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
+
+ qrCodeView()
+
+ Section {
+ IncognitoToggle(incognitoEnabled: $incognitoDefault)
+ } footer: {
+ sharedProfileInfo(incognitoDefault)
+ }
+ }
+ .onChange(of: incognitoDefault) { incognito in
+ Task {
+ do {
+ if let contactConn = contactConnection,
+ let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
+ await MainActor.run {
+ contactConnection = conn
+ chatModel.updateContactConnection(conn)
+ }
+ }
+ } catch {
+ logger.error("apiSetConnectionIncognito error: \(responseError(error))")
+ }
+ }
+ setInvitationUsed()
+ }
+ }
+
+ private func shareLinkView() -> some View {
+ HStack {
+ let link = simplexChatLink(connReqInvitation)
+ linkTextView(link)
+ Button {
+ showShareSheet(items: [link])
+ setInvitationUsed()
+ } label: {
+ Image(systemName: "square.and.arrow.up")
+ .padding(.top, -7)
+ }
+ }
+ .frame(maxWidth: .infinity)
+ }
+
+ private func qrCodeView() -> some View {
+ Section("Or show this code") {
+ SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed)
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .fill(Color(uiColor: .secondarySystemGroupedBackground))
+ )
+ .padding(.horizontal)
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ }
+ }
+
+ private func setInvitationUsed() {
+ if !invitationUsed {
+ invitationUsed = true
+ }
+ }
+}
+
+private struct ConnectView: View {
+ @Environment(\.dismiss) var dismiss: DismissAction
+ @State var showQRCodeScanner = false
+ @State private var cameraAuthorizationStatus: AVAuthorizationStatus?
+ @Binding var pastedLink: String
+ @Binding var alert: NewChatViewAlert?
+ @State private var sheet: PlanAndConnectActionSheet?
+
+ var body: some View {
+ List {
+ Section("Paste the link you received") {
+ pasteLinkView()
+ }
+
+ scanCodeView()
+ }
+ .actionSheet(item: $sheet) { s in
+ planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" })
+ }
+ .onAppear {
+ let status = AVCaptureDevice.authorizationStatus(for: .video)
+ cameraAuthorizationStatus = status
+ if showQRCodeScanner {
+ switch status {
+ case .notDetermined: askCameraAuthorization()
+ case .restricted: showQRCodeScanner = false
+ case .denied: showQRCodeScanner = false
+ case .authorized: ()
+ @unknown default: askCameraAuthorization()
+ }
+ }
+ }
+ }
+
+ func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
+ AVCaptureDevice.requestAccess(for: .video) { allowed in
+ cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
+ if allowed { cb?() }
+ }
+ }
+
+ @ViewBuilder private func pasteLinkView() -> some View {
+ if pastedLink == "" {
+ Button {
+ if let str = UIPasteboard.general.string {
+ if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) {
+ pastedLink = link.text
+ // It would be good to hide it, but right now it is not clear how to release camera in CodeScanner
+ // https://github.com/twostraws/CodeScanner/issues/121
+ // No known tricks worked (changing view ID, wrapping it in another view, etc.)
+ // showQRCodeScanner = false
+ connect(pastedLink)
+ } else {
+ alert = .newChatSomeAlert(alert: .someAlert(
+ alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
+ id: "pasteLinkView: code is not a SimpleX link"
+ ))
+ }
+ }
+ } label: {
+ Text("Tap to paste link")
+ }
+ .disabled(!ChatModel.shared.pasteboardHasStrings)
+ .frame(maxWidth: .infinity, alignment: .center)
+ } else {
+ linkTextView(pastedLink)
+ }
+ }
+
+ private func scanCodeView() -> some View {
+ Section("Or scan QR code") {
+ if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
+ CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
+ .aspectRatio(1, contentMode: .fit)
+ .cornerRadius(12)
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ .padding(.horizontal)
+ } else {
+ Button {
+ switch cameraAuthorizationStatus {
+ case .notDetermined: askCameraAuthorization { showQRCodeScanner = true }
+ case .restricted: ()
+ case .denied: UIApplication.shared.open(appSettingsURL)
+ case .authorized: showQRCodeScanner = true
+ default: askCameraAuthorization { showQRCodeScanner = true }
+ }
+ } label: {
+ ZStack {
+ Rectangle()
+ .aspectRatio(contentMode: .fill)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .foregroundColor(Color.clear)
+ switch cameraAuthorizationStatus {
+ case .restricted: Text("Camera not available")
+ case .denied: Label("Enable camera access", systemImage: "camera")
+ default: Label("Tap to scan", systemImage: "qrcode")
+ }
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
+ .padding()
+ .background(
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .fill(Color(uiColor: .secondarySystemGroupedBackground))
+ )
+ .padding(.horizontal)
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ .disabled(cameraAuthorizationStatus == .restricted)
+ }
+ }
+ }
+
+ private func processQRCode(_ resp: Result) {
+ switch resp {
+ case let .success(r):
+ let link = r.string
+ if strIsSimplexLink(r.string) {
+ connect(link)
+ } else {
+ alert = .newChatSomeAlert(alert: .someAlert(
+ alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
+ id: "processQRCode: code is not a SimpleX link"
+ ))
+ }
+ case let .failure(e):
+ logger.error("processQRCode QR code error: \(e.localizedDescription)")
+ alert = .newChatSomeAlert(alert: .someAlert(
+ alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
+ id: "processQRCode: failure"
+ ))
+ }
+ }
+
+ private func connect(_ link: String) {
+ planAndConnect(
+ link,
+ showAlert: { alert = .planAndConnectAlert(alert: $0) },
+ showActionSheet: { sheet = $0 },
+ dismiss: true,
+ incognito: nil
+ )
+ }
+}
+
+private func linkTextView(_ link: String) -> some View {
+ Text(link)
+ .lineLimit(1)
+ .font(.caption)
+ .truncationMode(.middle)
+}
+
+struct InfoSheetButton: View {
+ @ViewBuilder let content: Content
+ @State private var showInfoSheet = false
+
+ var body: some View {
+ Button {
+ showInfoSheet = true
+ } label: {
+ Image(systemName: "info.circle")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 24, height: 24)
+ }
+ .sheet(isPresented: $showInfoSheet) {
+ content
+ }
+ }
+}
+
+func strIsSimplexLink(_ str: String) -> Bool {
+ if let parsedMd = parseSimpleXMarkdown(str),
+ parsedMd.count == 1,
+ case .simplexLink = parsedMd[0].format {
+ return true
+ } else {
+ return false
+ }
+}
+
+func strHasSingleSimplexLink(_ str: String) -> FormattedText? {
+ if let parsedMd = parseSimpleXMarkdown(str) {
+ let parsedLinks = parsedMd.filter({ $0.format?.isSimplexLink ?? false })
+ if parsedLinks.count == 1 {
+ return parsedLinks[0]
+ } else {
+ return nil
+ }
+ } else {
+ return nil
+ }
+}
+
+struct IncognitoToggle: View {
+ @Binding var incognitoEnabled: Bool
+ @State private var showIncognitoSheet = false
+
+ var body: some View {
+ ZStack(alignment: .leading) {
+ Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks")
+ .frame(maxWidth: 24, maxHeight: 24, alignment: .center)
+ .foregroundColor(incognitoEnabled ? Color.indigo : .secondary)
+ .font(.system(size: 14))
+ Toggle(isOn: $incognitoEnabled) {
+ HStack(spacing: 6) {
+ Text("Incognito")
+ Image(systemName: "info.circle")
+ .foregroundColor(.accentColor)
+ .font(.system(size: 14))
+ }
+ .onTapGesture {
+ showIncognitoSheet = true
+ }
+ }
+ .padding(.leading, 36)
+ }
+ .sheet(isPresented: $showIncognitoSheet) {
+ IncognitoHelp()
+ }
+ }
+}
+
+func sharedProfileInfo(_ incognito: Bool) -> Text {
+ let name = ChatModel.shared.currentUser?.displayName ?? ""
+ return Text(
+ incognito
+ ? "A new random profile will be shared."
+ : "Your profile **\(name)** will be shared."
+ )
+}
+
+enum PlanAndConnectAlert: Identifiable {
+ case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case invitationLinkConnecting(connectionLink: String)
+ case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
+ case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
+
+ var id: String {
+ switch self {
+ case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
+ case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
+ case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
+ case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)"
+ case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
+ case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)"
+ case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
+ }
+ }
+}
+
+func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (() -> Void)? = nil) -> Alert {
+ switch alert {
+ case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Connect to yourself?"),
+ message: Text("This is your own one-time link!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Connect incognito" : "Connect"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
+ ),
+ secondaryButton: .cancel() { cleanup?() }
+ )
+ case .invitationLinkConnecting:
+ return Alert(
+ title: Text("Already connecting!"),
+ message: Text("You are already connecting via this one-time link!"),
+ dismissButton: .default(Text("OK")) { cleanup?() }
+ )
+ case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Connect to yourself?"),
+ message: Text("This is your own SimpleX address!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Connect incognito" : "Connect"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
+ ),
+ secondaryButton: .cancel() { cleanup?() }
+ )
+ case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Repeat connection request?"),
+ message: Text("You have already requested connection via this address!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Connect incognito" : "Connect"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
+ ),
+ secondaryButton: .cancel() { cleanup?() }
+ )
+ case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Join group?"),
+ message: Text("You will connect to all group members."),
+ primaryButton: .default(
+ Text(incognito ? "Join incognito" : "Join"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
+ ),
+ secondaryButton: .cancel() { cleanup?() }
+ )
+ case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito):
+ return Alert(
+ title: Text("Repeat join request?"),
+ message: Text("You are already joining the group via this link!"),
+ primaryButton: .destructive(
+ Text(incognito ? "Join incognito" : "Join"),
+ action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }
+ ),
+ secondaryButton: .cancel() { cleanup?() }
+ )
+ case let .groupLinkConnecting(_, groupInfo):
+ if let groupInfo = groupInfo {
+ return Alert(
+ title: Text("Group already exists!"),
+ message: Text("You are already joining the group \(groupInfo.displayName)."),
+ dismissButton: .default(Text("OK")) { cleanup?() }
+ )
+ } else {
+ return Alert(
+ title: Text("Already joining the group!"),
+ message: Text("You are already joining the group via this link."),
+ dismissButton: .default(Text("OK")) { cleanup?() }
+ )
+ }
+ }
+}
+
+enum PlanAndConnectActionSheet: Identifiable {
+ case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
+ case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
+ case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact)
+ case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
+
+ var id: String {
+ switch self {
+ case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
+ case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)"
+ case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)"
+ case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
+ }
+ }
+}
+
+func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool, cleanup: (() -> Void)? = nil) -> ActionSheet {
+ switch sheet {
+ case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
+ return ActionSheet(
+ title: Text(title),
+ buttons: [
+ .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) },
+ .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) },
+ .cancel() { cleanup?() }
+ ]
+ )
+ case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title):
+ return ActionSheet(
+ title: Text(title),
+ buttons: [
+ .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) },
+ .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) },
+ .cancel() { cleanup?() }
+ ]
+ )
+ case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact):
+ return ActionSheet(
+ title: Text("Connect with \(contact.chatViewName)"),
+ buttons: [
+ .default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) },
+ .default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) },
+ .cancel() { cleanup?() }
+ ]
+ )
+ case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo):
+ if let incognito = incognito {
+ return ActionSheet(
+ title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
+ buttons: [
+ .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
+ .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) },
+ .cancel() { cleanup?() }
+ ]
+ )
+ } else {
+ return ActionSheet(
+ title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
+ buttons: [
+ .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
+ .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) },
+ .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) },
+ .cancel() { cleanup?() }
+ ]
+ )
+ }
+ }
+}
+
+func planAndConnect(
+ _ connectionLink: String,
+ showAlert: @escaping (PlanAndConnectAlert) -> Void,
+ showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
+ dismiss: Bool,
+ incognito: Bool?,
+ cleanup: (() -> Void)? = nil,
+ filterKnownContact: ((Contact) -> Void)? = nil,
+ filterKnownGroup: ((GroupInfo) -> Void)? = nil
+) {
+ Task {
+ do {
+ let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
+ switch connectionPlan {
+ case let .invitationLink(ilp):
+ switch ilp {
+ case .ok:
+ logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
+ }
+ case .ownLink:
+ logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
+ }
+ case let .connecting(contact_):
+ logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
+ if let contact = contact_ {
+ if let f = filterKnownContact {
+ f(contact)
+ } else {
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
+ }
+ } else {
+ showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
+ }
+ case let .known(contact):
+ logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
+ if let f = filterKnownContact {
+ f(contact)
+ } else {
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
+ }
+ }
+ case let .contactAddress(cap):
+ switch cap {
+ case .ok:
+ logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
+ }
+ case .ownLink:
+ logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
+ }
+ case .connectingConfirmReconnect:
+ logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
+ }
+ case let .connectingProhibit(contact):
+ logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
+ if let f = filterKnownContact {
+ f(contact)
+ } else {
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
+ }
+ case let .known(contact):
+ logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
+ if let f = filterKnownContact {
+ f(contact)
+ } else {
+ openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
+ }
+ case let .contactViaAddress(contact):
+ logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
+ }
+ }
+ case let .groupLink(glp):
+ switch glp {
+ case .ok:
+ if let incognito = incognito {
+ showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
+ }
+ case let .ownLink(groupInfo):
+ logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
+ if let f = filterKnownGroup {
+ f(groupInfo)
+ }
+ showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
+ case .connectingConfirmReconnect:
+ logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
+ if let incognito = incognito {
+ showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
+ }
+ case let .connectingProhibit(groupInfo_):
+ logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
+ showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
+ case let .known(groupInfo):
+ logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
+ if let f = filterKnownGroup {
+ f(groupInfo)
+ } else {
+ openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
+ }
+ }
+ }
+ } catch {
+ logger.debug("planAndConnect, plan error")
+ if let incognito = incognito {
+ connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
+ } else {
+ showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
+ }
+ }
+ }
+}
+
+private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incognito: Bool, cleanup: (() -> Void)? = nil) {
+ Task {
+ if dismiss {
+ DispatchQueue.main.async {
+ dismissAllSheets(animated: true)
+ }
+ }
+ _ = await connectContactViaAddress(contact.contactId, incognito)
+ cleanup?()
+ }
+}
+
+private func connectViaLink(
+ _ connectionLink: String,
+ connectionPlan: ConnectionPlan?,
+ dismiss: Bool,
+ incognito: Bool,
+ cleanup: (() -> Void)?
+) {
+ Task {
+ if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) {
+ await MainActor.run {
+ ChatModel.shared.updateContactConnection(pcc)
+ }
+ let crt: ConnReqType
+ if let plan = connectionPlan {
+ crt = planToConnReqType(plan)
+ } else {
+ crt = connReqType
+ }
+ DispatchQueue.main.async {
+ if dismiss {
+ dismissAllSheets(animated: true) {
+ AlertManager.shared.showAlert(connReqSentAlert(crt))
+ }
+ } else {
+ AlertManager.shared.showAlert(connReqSentAlert(crt))
+ }
+ }
+ } else {
+ if dismiss {
+ DispatchQueue.main.async {
+ dismissAllSheets(animated: true)
+ }
+ }
+ }
+ cleanup?()
+ }
+}
+
+func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
+ Task {
+ let m = ChatModel.shared
+ if let c = m.getContactChat(contact.contactId) {
+ DispatchQueue.main.async {
+ if dismiss {
+ dismissAllSheets(animated: true) {
+ m.chatId = c.id
+ showAlreadyExistsAlert?()
+ }
+ } else {
+ m.chatId = c.id
+ showAlreadyExistsAlert?()
+ }
+ }
+ }
+ }
+}
+
+func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
+ Task {
+ let m = ChatModel.shared
+ if let g = m.getGroupChat(groupInfo.groupId) {
+ DispatchQueue.main.async {
+ if dismiss {
+ dismissAllSheets(animated: true) {
+ m.chatId = g.id
+ showAlreadyExistsAlert?()
+ }
+ } else {
+ m.chatId = g.id
+ showAlreadyExistsAlert?()
+ }
+ }
+ }
+ }
+}
+
+func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
+ mkAlert(
+ title: "Contact already exists",
+ message: "You are already connecting to \(contact.displayName)."
+ )
+}
+
+func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
+ mkAlert(
+ title: "Group already exists",
+ message: "You are already in group \(groupInfo.displayName)."
+ )
+}
+
+enum ConnReqType: Equatable {
+ case invitation
+ case contact
+ case groupLink
+
+ var connReqSentText: LocalizedStringKey {
+ switch self {
+ case .invitation: return "You will be connected when your contact's device is online, please wait or check later!"
+ case .contact: return "You will be connected when your connection request is accepted, please wait or check later!"
+ case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!"
+ }
+ }
+}
+
+private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
+ switch connectionPlan {
+ case .invitationLink: return .invitation
+ case .contactAddress: return .contact
+ case .groupLink: return .groupLink
+ }
+}
+
+func connReqSentAlert(_ type: ConnReqType) -> Alert {
+ return mkAlert(
+ title: "Connection request sent!",
+ message: type.connReqSentText
+ )
+}
+
+#Preview {
+ NewChatView(
+ selection: .invite
+ )
+}
diff --git a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift
deleted file mode 100644
index 7c272fb631..0000000000
--- a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift
+++ /dev/null
@@ -1,106 +0,0 @@
-//
-// PasteToConnectView.swift
-// SimpleX (iOS)
-//
-// Created by Ian Davies on 22/04/2022.
-// Copyright © 2022 SimpleX Chat. All rights reserved.
-//
-
-import SwiftUI
-import SimpleXChat
-
-struct PasteToConnectView: View {
- @Environment(\.dismiss) var dismiss: DismissAction
- @State private var connectionLink: String = ""
- @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
- @FocusState private var linkEditorFocused: Bool
- @State private var alert: PlanAndConnectAlert?
- @State private var sheet: PlanAndConnectActionSheet?
-
- var body: some View {
- List {
- Text("Connect via link")
- .font(.largeTitle)
- .bold()
- .fixedSize(horizontal: false, vertical: true)
- .listRowBackground(Color.clear)
- .listRowSeparator(.hidden)
- .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
- .onTapGesture { linkEditorFocused = false }
-
- Section {
- linkEditor()
-
- Button {
- if connectionLink == "" {
- connectionLink = UIPasteboard.general.string ?? ""
- } else {
- connectionLink = ""
- }
- } label: {
- if connectionLink == "" {
- settingsRow("doc.plaintext") { Text("Paste") }
- } else {
- settingsRow("multiply") { Text("Clear") }
- }
- }
-
- Button {
- connect()
- } label: {
- settingsRow("link") { Text("Connect") }
- }
- .disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil)
-
- IncognitoToggle(incognitoEnabled: $incognitoDefault)
- } footer: {
- VStack(alignment: .leading, spacing: 4) {
- sharedProfileInfo(incognitoDefault)
- Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- }
- .alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
- .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
- }
-
- private func linkEditor() -> some View {
- ZStack {
- Group {
- if connectionLink.isEmpty {
- TextEditor(text: Binding.constant(NSLocalizedString("Paste the link you received to connect with your contact.", comment: "placeholder")))
- .foregroundColor(.secondary)
- .disabled(true)
- }
- TextEditor(text: $connectionLink)
- .onSubmit(connect)
- .textInputAutocapitalization(.never)
- .disableAutocorrection(true)
- .focused($linkEditorFocused)
- }
- .allowsTightening(false)
- .padding(.horizontal, -5)
- .padding(.top, -8)
- .frame(height: 180, alignment: .topLeading)
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- }
-
- private func connect() {
- let link = connectionLink.trimmingCharacters(in: .whitespaces)
- planAndConnect(
- link,
- showAlert: { alert = $0 },
- showActionSheet: { sheet = $0 },
- dismiss: true,
- incognito: incognitoDefault
- )
- }
-}
-
-struct PasteToConnectView_Previews: PreviewProvider {
- static var previews: some View {
- PasteToConnectView()
- }
-}
diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift
index 3ddb85079c..e3bae9287a 100644
--- a/apps/ios/Shared/Views/NewChat/QRCode.swift
+++ b/apps/ios/Shared/Views/NewChat/QRCode.swift
@@ -24,9 +24,10 @@ struct SimpleXLinkQRCode: View {
let uri: String
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
+ var onShare: (() -> Void)? = nil
var body: some View {
- QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor)
+ QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor, onShare: onShare)
}
}
@@ -40,6 +41,7 @@ struct QRCode: View {
let uri: String
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
+ var onShare: (() -> Void)? = nil
@State private var image: UIImage? = nil
@State private var makeScreenshotFunc: () -> Void = {}
@@ -65,6 +67,7 @@ struct QRCode: View {
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
+ onShare?()
}
}
.frame(width: geo.size.width, height: geo.size.height)
diff --git a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift
deleted file mode 100644
index 7f3f5e02f8..0000000000
--- a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift
+++ /dev/null
@@ -1,79 +0,0 @@
-//
-// ConnectContactView.swift
-// SimpleX
-//
-// Created by Evgeny Poberezkin on 29/01/2022.
-// Copyright © 2022 SimpleX Chat. All rights reserved.
-//
-
-import SwiftUI
-import SimpleXChat
-import CodeScanner
-
-struct ScanToConnectView: View {
- @Environment(\.dismiss) var dismiss: DismissAction
- @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
- @State private var alert: PlanAndConnectAlert?
- @State private var sheet: PlanAndConnectActionSheet?
-
- var body: some View {
- ScrollView {
- VStack(alignment: .leading) {
- Text("Scan QR code")
- .font(.largeTitle)
- .bold()
- .fixedSize(horizontal: false, vertical: true)
- .padding(.vertical)
-
- CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
- .aspectRatio(1, contentMode: .fit)
- .cornerRadius(12)
-
- IncognitoToggle(incognitoEnabled: $incognitoDefault)
- .padding(.horizontal)
- .padding(.vertical, 6)
- .background(
- RoundedRectangle(cornerRadius: 12, style: .continuous)
- .fill(Color(uiColor: .systemBackground))
- )
- .padding(.top)
-
- VStack(alignment: .leading, spacing: 4) {
- sharedProfileInfo(incognitoDefault)
- Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.")
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .font(.footnote)
- .foregroundColor(.secondary)
- .padding(.horizontal)
- }
- .padding()
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
- }
- .background(Color(.systemGroupedBackground))
- .alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
- .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
- }
-
- func processQRCode(_ resp: Result) {
- switch resp {
- case let .success(r):
- planAndConnect(
- r.string,
- showAlert: { alert = $0 },
- showActionSheet: { sheet = $0 },
- dismiss: true,
- incognito: incognitoDefault
- )
- case let .failure(e):
- logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
- dismiss()
- }
- }
-}
-
-struct ConnectContactView_Previews: PreviewProvider {
- static var previews: some View {
- ScanToConnectView()
- }
-}
diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
index f5db37dacf..3f835e25d4 100644
--- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
+++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
@@ -11,12 +11,14 @@ import SimpleXChat
enum UserProfileAlert: Identifiable {
case duplicateUserError
+ case invalidDisplayNameError
case createUserError(error: LocalizedStringKey)
case invalidNameError(validName: String)
var id: String {
switch self {
case .duplicateUserError: return "duplicateUserError"
+ case .invalidDisplayNameError: return "invalidDisplayNameError"
case .createUserError: return "createUserError"
case let .invalidNameError(validName): return "invalidNameError \(validName)"
}
@@ -187,6 +189,12 @@ private func createProfile(_ displayName: String, showAlert: (UserProfileAlert)
} else {
showAlert(.duplicateUserError)
}
+ case .chatCmdError(_, .error(.invalidDisplayName)):
+ if m.currentUser == nil {
+ AlertManager.shared.showAlert(invalidDisplayNameAlert)
+ } else {
+ showAlert(.invalidDisplayNameError)
+ }
default:
let err: LocalizedStringKey = "Error: \(responseError(error))"
if m.currentUser == nil {
@@ -207,6 +215,7 @@ private func canCreateProfile(_ displayName: String) -> Bool {
func userProfileAlert(_ alert: UserProfileAlert, _ displayName: Binding) -> Alert {
switch alert {
case .duplicateUserError: return duplicateUserAlert
+ case .invalidDisplayNameError: return invalidDisplayNameAlert
case let .createUserError(err): return creatUserErrorAlert(err)
case let .invalidNameError(name): return createInvalidNameAlert(name, displayName)
}
@@ -219,6 +228,13 @@ private var duplicateUserAlert: Alert {
)
}
+private var invalidDisplayNameAlert: Alert {
+ Alert(
+ title: Text("Invalid display name!"),
+ message: Text("This display name is invalid. Please choose another name.")
+ )
+}
+
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
Alert(
title: Text("Error creating profile!"),
diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
index 59c2b25b6d..ece10e46dd 100644
--- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
+++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
@@ -314,6 +314,37 @@ private let versionDescriptions: [VersionDescription] = [
),
]
),
+ VersionDescription(
+ version: "v5.5",
+ post: URL(string: "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html"),
+ features: [
+ FeatureDescription(
+ icon: "folder",
+ title: "Private notes",
+ description: "With encrypted files and media."
+ ),
+ FeatureDescription(
+ icon: "link",
+ title: "Paste link to connect!",
+ description: "Search bar accepts invitation links."
+ ),
+ FeatureDescription(
+ icon: "bubble.left.and.bubble.right",
+ title: "Join group conversations",
+ description: "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."
+ ),
+ FeatureDescription(
+ icon: "battery.50",
+ title: "Improved message delivery",
+ description: "With reduced battery usage."
+ ),
+ FeatureDescription(
+ icon: "character",
+ title: "Turkish interface",
+ description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
+ ),
+ ]
+ )
]
private let lastVersion = versionDescriptions.last!.version
diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
index 20dadb7954..fc478596a9 100644
--- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
+++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
@@ -10,24 +10,23 @@ import SwiftUI
struct IncognitoHelp: View {
var body: some View {
- VStack(alignment: .leading) {
+ List {
Text("Incognito mode")
.font(.largeTitle)
.bold()
+ .fixedSize(horizontal: false, vertical: true)
.padding(.vertical)
- ScrollView {
- VStack(alignment: .leading) {
- Group {
- Text("Incognito mode protects your privacy by using a new random profile for each contact.")
- Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
- Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
- }
- .padding(.bottom)
- }
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ VStack(alignment: .leading, spacing: 18) {
+ Text("Incognito mode protects your privacy by using a new random profile for each contact.")
+ Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
+ Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
+ Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).")
}
+ .listRowBackground(Color.clear)
}
- .frame(maxWidth: .infinity)
- .padding()
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift
index 960afb6d38..2e560f8578 100644
--- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift
@@ -63,7 +63,6 @@ struct PreferencesView: View {
private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding) -> some View {
Text(feature.allowDescription(allowFeature.wrappedValue))
- .frame(height: 36, alignment: .topLeading)
}
private func savePreferences() {
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index d8ff2c2f89..8d13c6fb39 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -491,14 +491,23 @@ struct SimplexLockView: View {
showLAAlert(.laPasscodeNotChangedAlert)
}
case .enableSelfDestruct:
- SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) {
+ SetAppPasscodeView(
+ passcodeKeychain: kcSelfDestructPassword,
+ prohibitedPasscodeKeychain: kcAppPassword,
+ title: "Set passcode",
+ reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")
+ ) {
updateSelfDestruct()
showLAAlert(.laSelfDestructPasscodeSetAlert)
} cancel: {
revertSelfDestruct()
}
case .changeSelfDestructPasscode:
- SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) {
+ SetAppPasscodeView(
+ passcodeKeychain: kcSelfDestructPassword,
+ prohibitedPasscodeKeychain: kcAppPassword,
+ reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")
+ ) {
showLAAlert(.laSelfDestructPasscodeChangedAlert)
} cancel: {
showLAAlert(.laPasscodeNotChangedAlert)
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index f889d9c394..a691e6afc9 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -95,6 +95,12 @@ let appDefaults: [String: Any] = [
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
]
+// not used anymore
+enum ConnectViaLinkTab: String {
+ case scan
+ case paste
+}
+
enum SimpleXLinkMode: String, Identifiable {
case description
case full
@@ -153,37 +159,42 @@ struct SettingsView: View {
}
@ViewBuilder func settingsView() -> some View {
- let user: User = chatModel.currentUser!
+ let user = chatModel.currentUser
NavigationView {
List {
Section("You") {
- NavigationLink {
- UserProfile()
- .navigationTitle("Your current profile")
- } label: {
- ProfilePreview(profileOf: user)
- .padding(.leading, -8)
+ if let user = user {
+ NavigationLink {
+ UserProfile()
+ .navigationTitle("Your current profile")
+ } label: {
+ ProfilePreview(profileOf: user)
+ .padding(.leading, -8)
+ }
}
NavigationLink {
- UserProfilesView()
+ UserProfilesView(showSettings: $showSettings)
} label: {
settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") }
}
- NavigationLink {
- UserAddressView(shareViaProfile: chatModel.currentUser!.addressShared)
- .navigationTitle("SimpleX address")
- .navigationBarTitleDisplayMode(.large)
- } label: {
- settingsRow("qrcode") { Text("Your SimpleX address") }
- }
- NavigationLink {
- PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
- .navigationTitle("Your preferences")
- } label: {
- settingsRow("switch.2") { Text("Chat preferences") }
+ if let user = user {
+ NavigationLink {
+ UserAddressView(shareViaProfile: user.addressShared)
+ .navigationTitle("SimpleX address")
+ .navigationBarTitleDisplayMode(.large)
+ } label: {
+ settingsRow("qrcode") { Text("Your SimpleX address") }
+ }
+
+ NavigationLink {
+ PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
+ .navigationTitle("Your preferences")
+ } label: {
+ settingsRow("switch.2") { Text("Chat preferences") }
+ }
}
NavigationLink {
@@ -244,12 +255,14 @@ struct SettingsView: View {
}
Section("Help") {
- NavigationLink {
- ChatHelp(showSettings: $showSettings)
- .navigationTitle("Welcome \(user.displayName)!")
- .frame(maxHeight: .infinity, alignment: .top)
- } label: {
- settingsRow("questionmark") { Text("How to use it") }
+ if let user = user {
+ NavigationLink {
+ ChatHelp(showSettings: $showSettings)
+ .navigationTitle("Welcome \(user.displayName)!")
+ .frame(maxHeight: .infinity, alignment: .top)
+ } label: {
+ settingsRow("questionmark") { Text("How to use it") }
+ }
}
NavigationLink {
WhatsNewView(viaSettings: true)
diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
index f6c7bf37e8..f2cac59dae 100644
--- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
@@ -8,6 +8,7 @@ import SimpleXChat
struct UserProfilesView: View {
@EnvironmentObject private var m: ChatModel
+ @Binding var showSettings: Bool
@Environment(\.editMode) private var editMode
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
@@ -25,7 +26,6 @@ struct UserProfilesView: View {
private enum UserProfilesAlert: Identifiable {
case deleteUser(user: User, delSMPQueues: Bool)
- case cantDeleteLastUser
case hiddenProfilesNotice
case muteProfileAlert
case activateUserError(error: String)
@@ -34,7 +34,6 @@ struct UserProfilesView: View {
var id: String {
switch self {
case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)"
- case .cantDeleteLastUser: return "cantDeleteLastUser"
case .hiddenProfilesNotice: return "hiddenProfilesNotice"
case .muteProfileAlert: return "muteProfileAlert"
case let .activateUserError(err): return "activateUserError \(err)"
@@ -78,7 +77,7 @@ struct UserProfilesView: View {
Section {
let users = filteredUsers()
let v = ForEach(users) { u in
- userView(u.user, allowDelete: users.count > 1)
+ userView(u.user)
}
if #available(iOS 16, *) {
v.onDelete { indexSet in
@@ -146,13 +145,6 @@ struct UserProfilesView: View {
},
secondaryButton: .cancel()
)
- case .cantDeleteLastUser:
- return Alert(
- title: Text("Can't delete user profile!"),
- message: m.users.count > 1
- ? Text("There should be at least one visible user profile.")
- : Text("There should be at least one user profile.")
- )
case .hiddenProfilesNotice:
return Alert(
title: Text("Make profile private!"),
@@ -280,11 +272,21 @@ struct UserProfilesView: View {
if let newActive = m.users.first(where: { u in !u.user.activeUser && !u.user.hidden }) {
try await changeActiveUserAsync_(newActive.user.userId, viewPwd: nil)
try await deleteUser()
+ } else {
+ // Deleting the last visible user while having hidden one(s)
+ try await deleteUser()
+ try await changeActiveUserAsync_(nil, viewPwd: nil)
+ await MainActor.run {
+ onboardingStageDefault.set(.step1_SimpleXInfo)
+ m.onboardingStage = .step1_SimpleXInfo
+ showSettings = false
+ }
}
} else {
try await deleteUser()
}
} catch let error {
+ logger.error("Error deleting user profile: \(error)")
let a = getErrorAlert(error, "Error deleting user profile")
alert = .error(title: a.title, error: a.message)
}
@@ -295,7 +297,7 @@ struct UserProfilesView: View {
}
}
- @ViewBuilder private func userView(_ user: User, allowDelete: Bool) -> some View {
+ @ViewBuilder private func userView(_ user: User) -> some View {
let v = Button {
Task {
do {
@@ -323,9 +325,7 @@ struct UserProfilesView: View {
}
}
}
- .disabled(user.activeUser)
.foregroundColor(.primary)
- .deleteDisabled(!allowDelete)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if user.hidden {
Button("Unhide") {
@@ -361,8 +361,6 @@ struct UserProfilesView: View {
}
if #available(iOS 16, *) {
v
- } else if !allowDelete {
- v
} else {
v.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button("Delete", role: .destructive) {
@@ -373,12 +371,8 @@ struct UserProfilesView: View {
}
private func confirmDeleteUser(_ user: User) {
- if m.users.count > 1 && (user.hidden || visibleUsersCount > 1) {
- showDeleteConfirmation = true
- userToDelete = user
- } else {
- alert = .cantDeleteLastUser
- }
+ showDeleteConfirmation = true
+ userToDelete = user
}
private func setUserPrivacy(_ user: User, successAlert: UserProfilesAlert? = nil, _ api: @escaping () async throws -> User) {
@@ -409,6 +403,6 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String {
struct UserProfilesView_Previews: PreviewProvider {
static var previews: some View {
- UserProfilesView()
+ UserProfilesView(showSettings: Binding.constant(true))
}
}
diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
index 7a2afea082..85e4ebb985 100644
--- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
+++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
@@ -89,6 +89,7 @@
%@ and %@
+ %@ и %@
No comment provided by engineer.
@@ -103,6 +104,7 @@
%@ connected
+ %@ свързан
No comment provided by engineer.
@@ -132,6 +134,7 @@
%@, %@ and %lld members
+ %@, %@ и %lld членове
No comment provided by engineer.
@@ -201,6 +204,7 @@
%lld group events
+ %lld групови събития
No comment provided by engineer.
@@ -210,14 +214,21 @@
%lld messages blocked
+ %lld блокирани съобщения
+ No comment provided by engineer.
+
+
+ %lld messages blocked by admin
No comment provided by engineer.
%lld messages marked deleted
+ %lld съобщения, маркирани като изтрити
No comment provided by engineer.
%lld messages moderated by %@
+ %lld съобщения, модерирани от %@
No comment provided by engineer.
@@ -292,10 +303,12 @@
(new)
+ (ново)
No comment provided by engineer.
(this device v%@)
+ (това устройство v%@)
No comment provided by engineer.
@@ -303,14 +316,19 @@
)
No comment provided by engineer.
+
+ **Add contact**: to create a new invitation link, or connect via a link you received.
+ **Добави контакт**: за създаване на нов линк или свързване чрез получен линк за връзка.
+ No comment provided by engineer.
+
**Add new contact**: to create your one-time QR Code or link for your contact.
**Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт.
No comment provided by engineer.
-
- **Create link / QR code** for your contact to use.
- **Създай линк / QR код**, който вашият контакт да използва.
+
+ **Create group**: to create a new group.
+ **Създай група**: за създаване на нова група.
No comment provided by engineer.
@@ -323,11 +341,6 @@
**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението).
No comment provided by engineer.
-
- **Paste received link** or open it in the browser and tap **Open in mobile app**.
- **Поставете получения линк** или го отворете в браузъра и докоснете **Отваряне в мобилно приложение**.
- No comment provided by engineer.
-
**Please note**: you will NOT be able to recover or change passphrase if you lose it.
**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите.
@@ -338,11 +351,6 @@
**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е.
No comment provided by engineer.
-
- **Scan QR code**: to connect to your contact in person or via video call.
- **Сканирай QR код**: за да се свържете с вашия контакт лично или чрез видеообаждане.
- No comment provided by engineer.
-
**Warning**: Instant push notifications require passphrase saved in Keychain.
**Внимание**: Незабавните push известия изискват парола, запазена в Keychain.
@@ -372,7 +380,7 @@
- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- delivery receipts (up to 20 members).
- faster and more stable.
- - свържете се с [директория за услуги](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjd LW3%23%2F%3Fv%3D1-2%26dh %3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)!
+ - свържете се с [директория за услуги](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)!
- потвърждениe за доставка (до 20 члена).
- по-бързо и по-стабилно.
No comment provided by engineer.
@@ -390,6 +398,9 @@
- optionally notify deleted contacts.
- profile names with spaces.
- and more!
+ - по желание уведомете изтритите контакти.
+- имена на профили с интервали.
+- и още!
No comment provided by engineer.
@@ -408,6 +419,7 @@
0 sec
+ 0 сек
time to disappear
@@ -440,11 +452,6 @@
1 седмица
time interval
-
- 1-time link
- Еднократен линк
- No comment provided by engineer.
-
5 minutes
5 минути
@@ -560,6 +567,11 @@
Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.
No comment provided by engineer.
+
+ Add contact
+ Добави контакт
+ No comment provided by engineer.
+
Add preset servers
Добави предварително зададени сървъри
@@ -630,6 +642,10 @@
Всички членове на групата ще останат свързани.
No comment provided by engineer.
+
+ All messages will be deleted - this cannot be undone!
+ No comment provided by engineer.
+
All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.
Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас.
@@ -637,6 +653,7 @@
All new messages from %@ will be hidden!
+ Всички нови съобщения от %@ ще бъдат скрити!
No comment provided by engineer.
@@ -664,9 +681,9 @@
Позволи изчезващи съобщения само ако вашият контакт ги разрешава.
No comment provided by engineer.
-
- Allow irreversible message deletion only if your contact allows it to you.
- Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава.
+
+ Allow irreversible message deletion only if your contact allows it to you. (24 hours)
+ Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)
No comment provided by engineer.
@@ -689,9 +706,9 @@
Разреши изпращането на изчезващи съобщения.
No comment provided by engineer.
-
- Allow to irreversibly delete sent messages.
- Позволи необратимо изтриване на изпратените съобщения.
+
+ Allow to irreversibly delete sent messages. (24 hours)
+ Позволи необратимо изтриване на изпратените съобщения. (24 часа)
No comment provided by engineer.
@@ -724,9 +741,9 @@
Позволи на вашите контакти да ви се обаждат.
No comment provided by engineer.
-
- Allow your contacts to irreversibly delete sent messages.
- Позволи на вашите контакти да изтриват необратимо изпратените съобщения.
+
+ Allow your contacts to irreversibly delete sent messages. (24 hours)
+ Позволи на вашите контакти да изтриват необратимо изпратените съобщения. (24 часа)
No comment provided by engineer.
@@ -746,10 +763,12 @@
Already connecting!
+ В процес на свързване!
No comment provided by engineer.
Already joining the group!
+ Вече се присъединихте към групата!
No comment provided by engineer.
@@ -874,6 +893,7 @@
Bad desktop address
+ Грешен адрес на настолното устройство
No comment provided by engineer.
@@ -888,6 +908,7 @@
Better groups
+ По-добри групи
No comment provided by engineer.
@@ -897,18 +918,34 @@
Block
+ Блокирай
+ No comment provided by engineer.
+
+
+ Block for all
No comment provided by engineer.
Block group members
+ Блокиране на членове на групата
No comment provided by engineer.
Block member
+ Блокирай член
+ No comment provided by engineer.
+
+
+ Block member for all?
No comment provided by engineer.
Block member?
+ Блокирай члена?
+ No comment provided by engineer.
+
+
+ Blocked by admin
No comment provided by engineer.
@@ -916,9 +953,9 @@
И вие, и вашият контакт можете да добавяте реакции към съобщението.
No comment provided by engineer.
-
- Both you and your contact can irreversibly delete sent messages.
- И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения.
+
+ Both you and your contact can irreversibly delete sent messages. (24 hours)
+ И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения. (24 часа)
No comment provided by engineer.
@@ -956,9 +993,9 @@
Обаждания
No comment provided by engineer.
-
- Can't delete user profile!
- Потребителският профил не може да се изтрие!
+
+ Camera not available
+ Камерата е неодстъпна
No comment provided by engineer.
@@ -1072,6 +1109,11 @@
Чатът е спрян
No comment provided by engineer.
+
+ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.
+ Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.
+ No comment provided by engineer.
+
Chat preferences
Чат настройки
@@ -1117,6 +1159,10 @@
Изчисти разговора?
No comment provided by engineer.
+
+ Clear private notes?
+ No comment provided by engineer.
+
Clear verification
Изчисти проверката
@@ -1174,6 +1220,7 @@
Connect automatically
+ Автоматично свъзрване
No comment provided by engineer.
@@ -1183,24 +1230,31 @@
Connect to desktop
+ Свързване с настолно устройство
No comment provided by engineer.
Connect to yourself?
+ Свърване със себе си?
No comment provided by engineer.
Connect to yourself?
This is your own SimpleX address!
+ Свърване със себе си?
+Това е вашият личен SimpleX адрес!
No comment provided by engineer.
Connect to yourself?
This is your own one-time link!
+ Свърване със себе си?
+Това е вашят еднократен линк за връзка!
No comment provided by engineer.
Connect via contact address
+ Свързване чрез адрес за контакт
No comment provided by engineer.
@@ -1208,11 +1262,6 @@ This is your own one-time link!
Свърване чрез линк
No comment provided by engineer.
-
- Connect via link / QR code
- Свърване чрез линк/QR код
- No comment provided by engineer.
-
Connect via one-time link
Свързване чрез еднократен линк за връзка
@@ -1220,14 +1269,17 @@ This is your own one-time link!
Connect with %@
+ Свързване с %@
No comment provided by engineer.
Connected desktop
+ Свързано настолно устройство
No comment provided by engineer.
Connected to desktop
+ Свързан с настолно устройство
No comment provided by engineer.
@@ -1242,6 +1294,7 @@ This is your own one-time link!
Connecting to desktop
+ Свързване с настолно устройство
No comment provided by engineer.
@@ -1266,6 +1319,7 @@ This is your own one-time link!
Connection terminated
+ Връзката е прекратена
No comment provided by engineer.
@@ -1335,6 +1389,7 @@ This is your own one-time link!
Correct name to %@?
+ Поправи име на %@?
No comment provided by engineer.
@@ -1349,6 +1404,7 @@ This is your own one-time link!
Create a group using a random profile.
+ Създай група с автоматично генериран профилл.
No comment provided by engineer.
@@ -1363,6 +1419,7 @@ This is your own one-time link!
Create group
+ Създай група
No comment provided by engineer.
@@ -1380,13 +1437,9 @@ This is your own one-time link!
Създайте нов профил в [настолното приложение](https://simplex.chat/downloads/). 💻
No comment provided by engineer.
-
- Create one-time invitation link
- Създай линк за еднократна покана
- No comment provided by engineer.
-
Create profile
+ Създай профил
No comment provided by engineer.
@@ -1404,11 +1457,24 @@ This is your own one-time link!
Създай своя профил
No comment provided by engineer.
+
+ Created at
+ No comment provided by engineer.
+
+
+ Created at: %@
+ copied message info
+
Created on %@
Създаден на %@
No comment provided by engineer.
+
+ Creating link…
+ Линкът се създава…
+ No comment provided by engineer.
+
Current Passcode
Текущ kод за достъп
@@ -1549,6 +1615,7 @@ This is your own one-time link!
Delete %lld messages?
+ Изтриване на %lld съобщения?
No comment provided by engineer.
@@ -1578,6 +1645,7 @@ This is your own one-time link!
Delete and notify contact
+ Изтрий и уведоми контакт
No comment provided by engineer.
@@ -1613,6 +1681,8 @@ This is your own one-time link!
Delete contact?
This cannot be undone!
+ Изтрий контакт?
+Това не може да бъде отменено!
No comment provided by engineer.
@@ -1757,14 +1827,17 @@ This cannot be undone!
Desktop address
+ Адрес на настолно устройство
No comment provided by engineer.
Desktop app version %@ is not compatible with this app.
+ Версията на настолното приложение %@ не е съвместима с това приложение.
No comment provided by engineer.
Desktop devices
+ Настолни устройства
No comment provided by engineer.
@@ -1859,6 +1932,7 @@ This cannot be undone!
Disconnect desktop?
+ Прекъсни връзката с настолното устройство?
No comment provided by engineer.
@@ -1868,6 +1942,7 @@ This cannot be undone!
Discover via local network
+ Открий през локалната мрежа
No comment provided by engineer.
@@ -1880,6 +1955,11 @@ This cannot be undone!
Отложи
No comment provided by engineer.
+
+ Do not send history to new members.
+ Не изпращай история на нови членове.
+ No comment provided by engineer.
+
Don't create address
Не създавай адрес
@@ -1907,7 +1987,7 @@ This cannot be undone!
Duplicate display name!
- Дублирано показвано име!
+ Дублирано име!
No comment provided by engineer.
@@ -1950,6 +2030,11 @@ This cannot be undone!
Активиране на автоматично изтриване на съобщения?
No comment provided by engineer.
+
+ Enable camera access
+ Разреши достъпа до камерата
+ No comment provided by engineer.
+
Enable for all
Активиране за всички
@@ -2015,6 +2100,11 @@ This cannot be undone!
Криптирано съобщение или друго събитие
notification
+
+ Encrypted message: app is stopped
+ Криптирано съобщение: приложението е спряно
+ notification
+
Encrypted message: database error
Криптирано съобщение: грешка в базата данни
@@ -2042,10 +2132,12 @@ This cannot be undone!
Encryption re-negotiation error
+ Грешка при повторно договаряне на криптиране
message decrypt error item
Encryption re-negotiation failed.
+ Неуспешно повторно договаряне на криптирането.
No comment provided by engineer.
@@ -2060,6 +2152,7 @@ This cannot be undone!
Enter group name…
+ Въведи име на групата…
No comment provided by engineer.
@@ -2079,6 +2172,7 @@ This cannot be undone!
Enter this device name…
+ Въведи името на това устройство…
No comment provided by engineer.
@@ -2093,6 +2187,7 @@ This cannot be undone!
Enter your name…
+ Въведи своето име…
No comment provided by engineer.
@@ -2155,6 +2250,10 @@ This cannot be undone!
Грешка при създаване на контакт с член
No comment provided by engineer.
+
+ Error creating message
+ No comment provided by engineer.
+
Error creating profile!
Грешка при създаване на профил!
@@ -2240,6 +2339,11 @@ This cannot be undone!
Грешка при зареждане на %@ сървъри
No comment provided by engineer.
+
+ Error opening chat
+ Грешка при отваряне на чата
+ No comment provided by engineer.
+
Error receiving file
Грешка при получаване на файл
@@ -2280,6 +2384,11 @@ This cannot be undone!
Грешка при запазване на потребителска парола
No comment provided by engineer.
+
+ Error scanning code: %@
+ Грешка при сканиране на кода: %@
+ No comment provided by engineer.
+
Error sending email
Грешка при изпращане на имейл
@@ -2372,6 +2481,7 @@ This cannot be undone!
Expand
+ Разшири
chat item action
@@ -2406,6 +2516,7 @@ This cannot be undone!
Faster joining and more reliable messages.
+ По-бързо присъединяване и по-надеждни съобщения.
No comment provided by engineer.
@@ -2505,6 +2616,7 @@ This cannot be undone!
Found desktop
+ Намерено настолно устройство
No comment provided by engineer.
@@ -2529,6 +2641,7 @@ This cannot be undone!
Fully decentralized – visible only to members.
+ Напълно децентрализирана – видима е само за членовете.
No comment provided by engineer.
@@ -2553,15 +2666,17 @@ This cannot be undone!
Group already exists
+ Групата вече съществува
No comment provided by engineer.
Group already exists!
+ Групата вече съществува!
No comment provided by engineer.
Group display name
- Показвано име на групата
+ Име на групата
No comment provided by engineer.
@@ -2604,9 +2719,9 @@ This cannot be undone!
Членовете на групата могат да добавят реакции към съобщенията.
No comment provided by engineer.
-
- Group members can irreversibly delete sent messages.
- Членовете на групата могат необратимо да изтриват изпратените съобщения.
+
+ Group members can irreversibly delete sent messages. (24 hours)
+ Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)
No comment provided by engineer.
@@ -2714,6 +2829,11 @@ This cannot be undone!
История
No comment provided by engineer.
+
+ History is not sent to new members.
+ Историята не се изпраща на нови членове.
+ No comment provided by engineer.
+
How SimpleX works
Как работи SimpleX
@@ -2749,11 +2869,6 @@ This cannot be undone!
Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.
No comment provided by engineer.
-
- If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.
- Ако не можете да се срещнете на живо, можете да **сканирате QR код във видеообаждането** или вашият контакт може да сподели линк за покана.
- No comment provided by engineer.
-
If you enter this passcode when opening the app, all app data will be irreversibly removed!
Ако въведете този kод за достъп, когато отваряте приложението, всички данни от приложението ще бъдат необратимо изтрити!
@@ -2809,6 +2924,10 @@ This cannot be undone!
Импортиране на база данни
No comment provided by engineer.
+
+ Improved message delivery
+ No comment provided by engineer.
+
Improved privacy and security
Подобрена поверителност и сигурност
@@ -2831,6 +2950,7 @@ This cannot be undone!
Incognito groups
+ Инкогнито групи
No comment provided by engineer.
@@ -2865,6 +2985,7 @@ This cannot be undone!
Incompatible version
+ Несъвместима версия
No comment provided by engineer.
@@ -2909,13 +3030,34 @@ This cannot be undone!
Интерфейс
No comment provided by engineer.
+
+ Invalid QR code
+ Невалиден QR код
+ No comment provided by engineer.
+
Invalid connection link
Невалиден линк за връзка
No comment provided by engineer.
+
+ Invalid display name!
+ Невалидно име!
+ No comment provided by engineer.
+
+
+ Invalid link
+ Невалиден линк
+ No comment provided by engineer.
+
Invalid name!
+ Невалидно име!
+ No comment provided by engineer.
+
+
+ Invalid response
+ Невалиден отговор
No comment provided by engineer.
@@ -3009,8 +3151,13 @@ This cannot be undone!
Влез в групата
No comment provided by engineer.
+
+ Join group conversations
+ No comment provided by engineer.
+
Join group?
+ Влез в групата?
No comment provided by engineer.
@@ -3020,11 +3167,14 @@ This cannot be undone!
Join with current profile
+ Присъединяване с текущия профил
No comment provided by engineer.
Join your group?
This is your link for group %@!
+ Влез в твоята група?
+Това е вашят линк за група %@!
No comment provided by engineer.
@@ -3032,8 +3182,19 @@ This is your link for group %@!
Присъединяване към групата
No comment provided by engineer.
+
+ Keep
+ Запази
+ No comment provided by engineer.
+
Keep the app open to use it from desktop
+ Дръжте приложението отворено, за да го използвате от настолното устройство
+ No comment provided by engineer.
+
+
+ Keep unused invitation?
+ Запази неизползваната покана за връзка?
No comment provided by engineer.
@@ -3098,14 +3259,17 @@ This is your link for group %@!
Link mobile and desktop apps! 🔗
+ Свържете мобилни и настолни приложения! 🔗
No comment provided by engineer.
Linked desktop options
+ Настройки на запомнени настолни устройства
No comment provided by engineer.
Linked desktops
+ Запомнени настолни устройства
No comment provided by engineer.
@@ -3118,6 +3282,11 @@ This is your link for group %@!
Съобщения на живо
No comment provided by engineer.
+
+ Local
+ Локално
+ No comment provided by engineer.
+
Local name
Локално име
@@ -3260,6 +3429,7 @@ This is your link for group %@!
Messages from %@ will be shown!
+ Съобщенията от %@ ще бъдат показани!
No comment provided by engineer.
@@ -3357,6 +3527,11 @@ This is your link for group %@!
Нов kод за достъп
No comment provided by engineer.
+
+ New chat
+ Нов чат
+ No comment provided by engineer.
+
New contact request
Нова заявка за контакт
@@ -3379,7 +3554,7 @@ This is your link for group %@!
New display name
- Ново показвано име
+ Ново име
No comment provided by engineer.
@@ -3459,6 +3634,7 @@ This is your link for group %@!
Not compatible!
+ Несъвместим!
No comment provided by engineer.
@@ -3480,16 +3656,16 @@ This is your link for group %@!
- да деактивират членове (роля "наблюдател")
No comment provided by engineer.
+
+ OK
+ ОК
+ No comment provided by engineer.
+
Off
Изключено
No comment provided by engineer.
-
- Off (Local)
- Изключено (Локално)
- No comment provided by engineer.
-
Ok
Ок
@@ -3550,9 +3726,9 @@ This is your link for group %@!
Само вие можете да добавяте реакции на съобщенията.
No comment provided by engineer.
-
- Only you can irreversibly delete messages (your contact can mark them for deletion).
- Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване).
+
+ Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)
+ Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване). (24 часа)
No comment provided by engineer.
@@ -3575,9 +3751,9 @@ This is your link for group %@!
Само вашият контакт може да добавя реакции на съобщенията.
No comment provided by engineer.
-
- Only your contact can irreversibly delete messages (you can mark them for deletion).
- Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване).
+
+ Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)
+ Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване). (24 часа)
No comment provided by engineer.
@@ -3617,6 +3793,7 @@ This is your link for group %@!
Open group
+ Отвори група
No comment provided by engineer.
@@ -3629,9 +3806,19 @@ This is your link for group %@!
Протокол и код с отворен код – всеки може да оперира собствени сървъри.
No comment provided by engineer.
-
- Opening database…
- Отваряне на база данни…
+
+ Opening app…
+ Приложението се отваря…
+ No comment provided by engineer.
+
+
+ Or scan QR code
+ Или сканирай QR код
+ No comment provided by engineer.
+
+
+ Or show this code
+ Или покажи този код
No comment provided by engineer.
@@ -3674,13 +3861,13 @@ This is your link for group %@!
Парола за показване
No comment provided by engineer.
-
- Paste
- Постави
- No comment provided by engineer.
+
+ Past member %@
+ past/unknown group member
Paste desktop address
+ Постави адрес на настолно устройство
No comment provided by engineer.
@@ -3688,15 +3875,14 @@ This is your link for group %@!
Постави изображение
No comment provided by engineer.
-
- Paste received link
- Постави получения линк
+
+ Paste link to connect!
No comment provided by engineer.
-
- Paste the link you received to connect with your contact.
- Поставете линка, който сте получили, за да се свържете с вашия контакт.
- placeholder
+
+ Paste the link you received
+ Постави получения линк
+ No comment provided by engineer.
People can connect to you only via the links you share.
@@ -3733,6 +3919,13 @@ This is your link for group %@!
Моля, проверете вашите настройки и тези вашия за контакт.
No comment provided by engineer.
+
+ Please contact developers.
+Error: %@
+ Моля, свържете се с разработчиците.
+Грешка: %@
+ No comment provided by engineer.
+
Please contact group admin.
Моля, свържете се с груповия администартор.
@@ -3818,6 +4011,10 @@ This is your link for group %@!
Поверителни имена на файлове
No comment provided by engineer.
+
+ Private notes
+ name of notes to self
+
Profile and server connections
Профилни и сървърни връзки
@@ -3830,10 +4027,12 @@ This is your link for group %@!
Profile name
+ Име на профила
No comment provided by engineer.
Profile name:
+ Име на профила:
No comment provided by engineer.
@@ -3936,6 +4135,11 @@ This is your link for group %@!
Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).
No comment provided by engineer.
+
+ Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).
+ Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).
+ No comment provided by engineer.
+
Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).
Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends).
@@ -3991,6 +4195,10 @@ This is your link for group %@!
Получаване чрез
No comment provided by engineer.
+
+ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).
+ No comment provided by engineer.
+
Recipients see updates as you type them.
Получателите виждат актуализации, докато ги въвеждате.
@@ -4083,10 +4291,12 @@ This is your link for group %@!
Repeat connection request?
+ Изпрати отново заявката за свързване?
No comment provided by engineer.
Repeat join request?
+ Изпрати отново заявката за присъединяване?
No comment provided by engineer.
@@ -4144,6 +4354,11 @@ This is your link for group %@!
Грешка при възстановяване на базата данни
No comment provided by engineer.
+
+ Retry
+ Опитай отново
+ No comment provided by engineer.
+
Reveal
Покажи
@@ -4269,6 +4484,10 @@ This is your link for group %@!
Запазените WebRTC ICE сървъри ще бъдат премахнати
No comment provided by engineer.
+
+ Saved message
+ message info title
+
Scan QR code
Сканирай QR код
@@ -4276,6 +4495,7 @@ This is your link for group %@!
Scan QR code from desktop
+ Сканирай QR код от настолното устройство
No comment provided by engineer.
@@ -4298,6 +4518,15 @@ This is your link for group %@!
Търсене
No comment provided by engineer.
+
+ Search bar accepts invitation links.
+ No comment provided by engineer.
+
+
+ Search or paste SimpleX link
+ Търсене или поставяне на SimpleX линк
+ No comment provided by engineer.
+
Secure queue
Сигурна опашка
@@ -4403,6 +4632,11 @@ This is your link for group %@!
Изпрати от галерия или персонализирани клавиатури.
No comment provided by engineer.
+
+ Send up to 100 last messages to new members.
+ Изпращане до последните 100 съобщения на нови членове.
+ No comment provided by engineer.
+
Sender cancelled file transfer.
Подателят отмени прехвърлянето на файла.
@@ -4500,6 +4734,7 @@ This is your link for group %@!
Session code
+ Код на сесията
No comment provided by engineer.
@@ -4572,9 +4807,9 @@ This is your link for group %@!
Сподели линк
No comment provided by engineer.
-
- Share one-time invitation link
- Сподели линк за еднократна покана
+
+ Share this 1-time invite link
+ Сподели този еднократен линк за връзка
No comment provided by engineer.
@@ -4697,16 +4932,16 @@ This is your link for group %@!
Някой
notification title
-
- Start a new chat
- Започни нов чат
- No comment provided by engineer.
-
Start chat
Започни чат
No comment provided by engineer.
+
+ Start chat?
+ Стартирай чата?
+ No comment provided by engineer.
+
Start migration
Започни миграция
@@ -4814,6 +5049,7 @@ This is your link for group %@!
Tap to Connect
+ Докосни за свързване
No comment provided by engineer.
@@ -4831,6 +5067,16 @@ This is your link for group %@!
Докосни за инкогнито вход
No comment provided by engineer.
+
+ Tap to paste link
+ Докосни за поставяне на линк за връзка
+ No comment provided by engineer.
+
+
+ Tap to scan
+ Докосни за сканиране
+ No comment provided by engineer.
+
Tap to start a new chat
Докосни за започване на нов чат
@@ -4893,6 +5139,11 @@ It can happen because of some bug or when the connection is compromised.Опитът за промяна на паролата на базата данни не беше завършен.
No comment provided by engineer.
+
+ The code you scanned is not a SimpleX link QR code.
+ QR кодът, който сканирахте, не е SimpleX линк за връзка.
+ No comment provided by engineer.
+
The connection you accepted will be cancelled!
Връзката, която приехте, ще бъде отказана!
@@ -4958,21 +5209,16 @@ It can happen because of some bug or when the connection is compromised.Сървърите за нови връзки на текущия ви чат профил **%@**.
No comment provided by engineer.
+
+ The text you pasted is not a SimpleX link.
+ Текстът, който поставихте, не е SimpleX линк за връзка.
+ No comment provided by engineer.
+
Theme
Тема
No comment provided by engineer.
-
- There should be at least one user profile.
- Трябва да има поне един потребителски профил.
- No comment provided by engineer.
-
-
- There should be at least one visible user profile.
- Трябва да има поне един видим потребителски профил.
- No comment provided by engineer.
-
These settings are for your current profile **%@**.
Тези настройки са за текущия ви профил **%@**.
@@ -5000,6 +5246,12 @@ It can happen because of some bug or when the connection is compromised.
This device name
+ Името на това устройство
+ No comment provided by engineer.
+
+
+ This display name is invalid. Please choose another name.
+ Това име е невалидно. Моля, изберете друго име.
No comment provided by engineer.
@@ -5014,10 +5266,12 @@ It can happen because of some bug or when the connection is compromised.
This is your own SimpleX address!
+ Това е вашият личен SimpleX адрес!
No comment provided by engineer.
This is your own one-time link!
+ Това е вашят еднократен линк за връзка!
No comment provided by engineer.
@@ -5037,6 +5291,7 @@ It can happen because of some bug or when the connection is compromised.
To hide unwanted messages.
+ Скриване на нежелани съобщения.
No comment provided by engineer.
@@ -5101,16 +5356,15 @@ You will be prompted to complete authentication before this feature is enabled.<
Опит за свързване със сървъра, използван за получаване на съобщения от този контакт.
No comment provided by engineer.
+
+ Turkish interface
+ No comment provided by engineer.
+
Turn off
Изключи
No comment provided by engineer.
-
- Turn off notifications?
- Изключи известията?
- No comment provided by engineer.
-
Turn on
Включи
@@ -5123,14 +5377,25 @@ You will be prompted to complete authentication before this feature is enabled.<
Unblock
+ Отблокирай
+ No comment provided by engineer.
+
+
+ Unblock for all
No comment provided by engineer.
Unblock member
+ Отблокирай член
+ No comment provided by engineer.
+
+
+ Unblock member for all?
No comment provided by engineer.
Unblock member?
+ Отблокирай член?
No comment provided by engineer.
@@ -5197,10 +5462,12 @@ To connect, please ask your contact to create another connection link and check
Unlink
+ Забрави
No comment provided by engineer.
Unlink desktop?
+ Забрави настолно устройство?
No comment provided by engineer.
@@ -5223,6 +5490,11 @@ To connect, please ask your contact to create another connection link and check
Непрочетено
No comment provided by engineer.
+
+ Up to 100 last messages are sent to new members.
+ На новите членове се изпращат до последните 100 съобщения.
+ No comment provided by engineer.
+
Update
Актуализация
@@ -5295,6 +5567,7 @@ To connect, please ask your contact to create another connection link and check
Use from desktop
+ Използвай от настолно устройство
No comment provided by engineer.
@@ -5307,6 +5580,11 @@ To connect, please ask your contact to create another connection link and check
Използвай нов инкогнито профил
No comment provided by engineer.
+
+ Use only local notifications?
+ Използвай само локални известия?
+ No comment provided by engineer.
+
Use server
Използвай сървър
@@ -5329,10 +5607,12 @@ To connect, please ask your contact to create another connection link and check
Verify code with desktop
+ Потвръди кода с настолното устройство
No comment provided by engineer.
Verify connection
+ Потвръди връзките
No comment provided by engineer.
@@ -5342,6 +5622,7 @@ To connect, please ask your contact to create another connection link and check
Verify connections
+ Потвръди връзките
No comment provided by engineer.
@@ -5356,6 +5637,7 @@ To connect, please ask your contact to create another connection link and check
Via secure quantum resistant protocol.
+ Чрез сигурен квантово устойчив протокол.
No comment provided by engineer.
@@ -5383,6 +5665,11 @@ To connect, please ask your contact to create another connection link and check
Виж кода за сигурност
No comment provided by engineer.
+
+ Visible history
+ Видима история
+ chat feature
+
Voice messages
Гласови съобщения
@@ -5410,6 +5697,7 @@ To connect, please ask your contact to create another connection link and check
Waiting for desktop...
+ Изчакване на настолно устройство…
No comment provided by engineer.
@@ -5467,11 +5755,19 @@ To connect, please ask your contact to create another connection link and check
Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани.
No comment provided by engineer.
+
+ With encrypted files and media.
+ No comment provided by engineer.
+
With optional welcome message.
С незадължително съобщение при посрещане.
No comment provided by engineer.
+
+ With reduced battery usage.
+ No comment provided by engineer.
+
Wrong database passphrase
Грешна парола за базата данни
@@ -5504,7 +5800,7 @@ To connect, please ask your contact to create another connection link and check
You already have a chat profile with the same display name. Please choose another name.
- Вече имате чат профил със същото показвано име. Моля, изберете друго име.
+ Вече имате чат профил със същото име. Моля, изберете друго име.
No comment provided by engineer.
@@ -5514,31 +5810,39 @@ To connect, please ask your contact to create another connection link and check
You are already connecting to %@.
+ Вече се свързвате с %@.
No comment provided by engineer.
You are already connecting via this one-time link!
+ Вече се свързвате чрез този еднократен линк за връзка!
No comment provided by engineer.
You are already in group %@.
+ Вече сте в група %@.
No comment provided by engineer.
You are already joining the group %@.
+ Вече се присъединявате към групата %@.
No comment provided by engineer.
You are already joining the group via this link!
+ Вие вече се присъединявате към групата чрез този линк!
No comment provided by engineer.
You are already joining the group via this link.
+ Вие вече се присъединявате към групата чрез този линк.
No comment provided by engineer.
You are already joining the group!
Repeat join request?
+ Вече се присъединихте към групата!
+Изпрати отново заявката за присъединяване?
No comment provided by engineer.
@@ -5556,11 +5860,6 @@ Repeat join request?
Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението.
No comment provided by engineer.
-
- You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.
- Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона **Отваряне в мобилно приложение**.
- No comment provided by engineer.
-
You can create it later
Можете да го създадете по-късно
@@ -5581,6 +5880,11 @@ Repeat join request?
Можете да скриете или заглушите известията за потребителски профил - плъзнете надясно.
No comment provided by engineer.
+
+ You can make it visible to your SimpleX contacts via Settings.
+ Можете да го направите видим за вашите контакти в SimpleX чрез Настройки.
+ No comment provided by engineer.
+
You can now send messages to %@
Вече можете да изпращате съобщения до %@
@@ -5621,6 +5925,11 @@ Repeat join request?
Можете да използвате markdown за форматиране на съобщенията:
No comment provided by engineer.
+
+ You can view invitation link again in connection details.
+ Можете да видите отново линкът за покана в подробностите за връзката.
+ No comment provided by engineer.
+
You can't send messages!
Не може да изпращате съобщения!
@@ -5638,11 +5947,14 @@ Repeat join request?
You have already requested connection via this address!
+ Вече сте заявили връзка през този адрес!
No comment provided by engineer.
You have already requested connection!
Repeat connection request?
+ Вече сте направили заявката за връзка!
+Изпрати отново заявката за свързване?
No comment provided by engineer.
@@ -5697,6 +6009,7 @@ Repeat connection request?
You will be connected when group link host's device is online, please wait or check later!
+ Ще бъдете свързани, когато устройството на хоста на груповата връзка е онлайн, моля, изчакайте или проверете по-късно!
No comment provided by engineer.
@@ -5716,6 +6029,7 @@ Repeat connection request?
You will connect to all group members.
+ Ще се свържете с всички членове на групата.
No comment provided by engineer.
@@ -5805,13 +6119,6 @@ You can cancel this connection and remove the contact (and try later with a new
Вашите контакти могат да позволят пълното изтриване на съобщението.
No comment provided by engineer.
-
- Your contacts in SimpleX will see it.
-You can change it in Settings.
- Вашите контакти в SimpleX ще го видят.
-Можете да го промените в Настройки.
- No comment provided by engineer.
-
Your contacts will remain connected.
Вашите контакти ще останат свързани.
@@ -5839,6 +6146,7 @@ You can change it in Settings.
Your profile
+ Вашият профил
No comment provided by engineer.
@@ -5935,6 +6243,7 @@ SimpleX сървърите не могат да видят вашия профи
and %lld other events
+ и %lld други събития
No comment provided by engineer.
@@ -5944,6 +6253,7 @@ SimpleX сървърите не могат да видят вашия профи
author
+ автор
member role
@@ -5958,8 +6268,17 @@ SimpleX сървърите не могат да видят вашия профи
blocked
+ блокиран
No comment provided by engineer.
+
+ blocked %@
+ rcv group event chat item
+
+
+ blocked by admin
+ blocked chat item
+
bold
удебелен
@@ -6080,6 +6399,10 @@ SimpleX сървърите не могат да видят вашия профи
връзка:%@
connection information
+
+ contact %1$@ changed to %2$@
+ profile update event chat item
+
contact has e2e encryption
контактът има e2e криптиране
@@ -6132,6 +6455,7 @@ SimpleX сървърите не могат да видят вашия профи
deleted contact
+ изтрит контакт
rcv direct event chat item
@@ -6349,6 +6673,10 @@ SimpleX сървърите не могат да видят вашия профи
член
member role
+
+ member %1$@ changed to %2$@
+ profile update event chat item
+
connected
свързан
@@ -6471,6 +6799,14 @@ SimpleX сървърите не могат да видят вашия профи
отстранен %@
rcv group event chat item
+
+ removed contact address
+ profile update event chat item
+
+
+ removed profile picture
+ profile update event chat item
+
removed you
ви острани
@@ -6501,6 +6837,14 @@ SimpleX сървърите не могат да видят вашия профи
изпрати лично съобщение
No comment provided by engineer.
+
+ set new contact address
+ profile update event chat item
+
+
+ set new profile picture
+ profile update event chat item
+
starting…
стартиране…
@@ -6516,18 +6860,31 @@ SimpleX сървърите не могат да видят вашия профи
този контакт
notification title
+
+ unblocked %@
+ rcv group event chat item
+
unknown
неизвестен
connection info
+
+ unknown status
+ No comment provided by engineer.
+
updated group profile
актуализиран профил на групата
rcv group event chat item
+
+ updated profile
+ profile update event chat item
+
v%@
+ v%@
No comment provided by engineer.
@@ -6595,6 +6952,10 @@ SimpleX сървърите не могат да видят вашия профи
вие сте наблюдател
No comment provided by engineer.
+
+ you blocked %@
+ snd group event chat item
+
you changed address
променихте адреса
@@ -6635,6 +6996,10 @@ SimpleX сървърите не могат да видят вашия профи
споделихте еднократен инкогнито линк за връзка
chat list item description
+
+ you unblocked %@
+ snd group event chat item
+
you:
вие:
@@ -6669,6 +7034,7 @@ SimpleX сървърите не могат да видят вашия профи
SimpleX uses local network access to allow using user chat profile via desktop app on the same network.
+ SimpleX използва достъп до локална мрежа, за да позволи използването на потребителския чат профил чрез настолно приложение в същата мрежа.
Privacy - Local Network Usage Description
diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
index be8b23658b..a376c86ad1 100644
--- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
+++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
@@ -89,6 +89,7 @@
%@ and %@
+ %@ a %@
No comment provided by engineer.
@@ -103,6 +104,7 @@
%@ connected
+ %@ připojen
No comment provided by engineer.
@@ -212,6 +214,10 @@
%lld messages blocked
No comment provided by engineer.
+
+ %lld messages blocked by admin
+ No comment provided by engineer.
+
%lld messages marked deleted
No comment provided by engineer.
@@ -303,14 +309,17 @@
)
No comment provided by engineer.
+
+ **Add contact**: to create a new invitation link, or connect via a link you received.
+ No comment provided by engineer.
+
**Add new contact**: to create your one-time QR Code or link for your contact.
**Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt.
No comment provided by engineer.
-
- **Create link / QR code** for your contact to use.
- **Vytvořte odkaz / QR kód** pro váš kontakt.
+
+ **Create group**: to create a new group.
No comment provided by engineer.
@@ -323,11 +332,6 @@
**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte).
No comment provided by engineer.
-
- **Paste received link** or open it in the browser and tap **Open in mobile app**.
- **Vložte přijatý odkaz** nebo jej otevřete v prohlížeči a klepněte na **Otevřít v mobilní aplikaci**.
- No comment provided by engineer.
-
**Please note**: you will NOT be able to recover or change passphrase if you lose it.
**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit.
@@ -338,11 +342,6 @@
**Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy.
No comment provided by engineer.
-
- **Scan QR code**: to connect to your contact in person or via video call.
- ** Naskenujte QR kód**: pro připojení ke kontaktu osobně nebo prostřednictvím videohovoru.
- No comment provided by engineer.
-
**Warning**: Instant push notifications require passphrase saved in Keychain.
**Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence.
@@ -372,7 +371,7 @@
- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- delivery receipts (up to 20 members).
- faster and more stable.
- - připojit k [adresářová služba](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.cibule) (BETA)!
+ - připojit k [adresářová služba](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- doručenky (až 20 členů).
- Rychlejší a stabilnější.
No comment provided by engineer.
@@ -440,11 +439,6 @@
1 týden
time interval
-
- 1-time link
- Jednorázový odkaz
- No comment provided by engineer.
-
5 minutes
5 minut
@@ -560,6 +554,10 @@
Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům.
No comment provided by engineer.
+
+ Add contact
+ No comment provided by engineer.
+
Add preset servers
Přidejte přednastavené servery
@@ -630,6 +628,10 @@
Všichni členové skupiny zůstanou připojeni.
No comment provided by engineer.
+
+ All messages will be deleted - this cannot be undone!
+ No comment provided by engineer.
+
All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.
Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás.
@@ -664,9 +666,9 @@
Povolte mizící zprávy, pouze pokud vám to váš kontakt dovolí.
No comment provided by engineer.
-
- Allow irreversible message deletion only if your contact allows it to you.
- Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí.
+
+ Allow irreversible message deletion only if your contact allows it to you. (24 hours)
+ Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin)
No comment provided by engineer.
@@ -689,9 +691,9 @@
Povolit odesílání mizících zpráv.
No comment provided by engineer.
-
- Allow to irreversibly delete sent messages.
- Povolit nevratné smazání odeslaných zpráv.
+
+ Allow to irreversibly delete sent messages. (24 hours)
+ Povolit nevratné smazání odeslaných zpráv. (24 hodin)
No comment provided by engineer.
@@ -724,9 +726,9 @@
Povolte svým kontaktům vám volat.
No comment provided by engineer.
-
- Allow your contacts to irreversibly delete sent messages.
- Umožněte svým kontaktům nevratně odstranit odeslané zprávy.
+
+ Allow your contacts to irreversibly delete sent messages. (24 hours)
+ Umožněte svým kontaktům nevratně odstranit odeslané zprávy. (24 hodin)
No comment provided by engineer.
@@ -899,6 +901,10 @@
Block
No comment provided by engineer.
+
+ Block for all
+ No comment provided by engineer.
+
Block group members
No comment provided by engineer.
@@ -907,18 +913,26 @@
Block member
No comment provided by engineer.
+
+ Block member for all?
+ No comment provided by engineer.
+
Block member?
No comment provided by engineer.
+
+ Blocked by admin
+ No comment provided by engineer.
+
Both you and your contact can add message reactions.
Vy i váš kontakt můžete přidávat reakce na zprávy.
No comment provided by engineer.
-
- Both you and your contact can irreversibly delete sent messages.
- Vy i váš kontakt můžete nevratně mazat odeslané zprávy.
+
+ Both you and your contact can irreversibly delete sent messages. (24 hours)
+ Vy i váš kontakt můžete nevratně mazat odeslané zprávy. (24 hodin)
No comment provided by engineer.
@@ -956,9 +970,8 @@
Hovory
No comment provided by engineer.
-
- Can't delete user profile!
- Nemohu smazat uživatelský profil!
+
+ Camera not available
No comment provided by engineer.
@@ -1072,6 +1085,10 @@
Chat je zastaven
No comment provided by engineer.
+
+ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.
+ No comment provided by engineer.
+
Chat preferences
Předvolby chatu
@@ -1117,6 +1134,10 @@
Vyčistit konverzaci?
No comment provided by engineer.
+
+ Clear private notes?
+ No comment provided by engineer.
+
Clear verification
Zrušte ověření
@@ -1208,11 +1229,6 @@ This is your own one-time link!
Připojte se prostřednictvím odkazu
No comment provided by engineer.
-
- Connect via link / QR code
- Připojit se prostřednictvím odkazu / QR kódu
- No comment provided by engineer.
-
Connect via one-time link
Připojit se jednorázovým odkazem
@@ -1380,11 +1396,6 @@ This is your own one-time link!
Vytvořit nový profil v [desktop app](https://simplex.chat/downloads/). 💻
No comment provided by engineer.
-
- Create one-time invitation link
- Vytvořit jednorázovou pozvánku
- No comment provided by engineer.
-
Create profile
No comment provided by engineer.
@@ -1404,11 +1415,23 @@ This is your own one-time link!
Vytvořte si profil
No comment provided by engineer.
+
+ Created at
+ No comment provided by engineer.
+
+
+ Created at: %@
+ copied message info
+
Created on %@
Vytvořeno na %@
No comment provided by engineer.
+
+ Creating link…
+ No comment provided by engineer.
+
Current Passcode
Aktuální heslo
@@ -1880,6 +1903,10 @@ This cannot be undone!
Udělat později
No comment provided by engineer.
+
+ Do not send history to new members.
+ No comment provided by engineer.
+
Don't create address
Nevytvářet adresu
@@ -1950,6 +1977,10 @@ This cannot be undone!
Povolit automatické mazání zpráv?
No comment provided by engineer.
+
+ Enable camera access
+ No comment provided by engineer.
+
Enable for all
Povolit pro všechny
@@ -2015,6 +2046,10 @@ This cannot be undone!
Šifrovaná zpráva nebo jiná událost
notification
+
+ Encrypted message: app is stopped
+ notification
+
Encrypted message: database error
Šifrovaná zpráva: chyba databáze
@@ -2155,6 +2190,10 @@ This cannot be undone!
Chyba vytvoření kontaktu člena
No comment provided by engineer.
+
+ Error creating message
+ No comment provided by engineer.
+
Error creating profile!
Chyba při vytváření profilu!
@@ -2240,6 +2279,10 @@ This cannot be undone!
Chyba načítání %@ serverů
No comment provided by engineer.
+
+ Error opening chat
+ No comment provided by engineer.
+
Error receiving file
Chyba při příjmu souboru
@@ -2280,6 +2323,10 @@ This cannot be undone!
Chyba ukládání hesla uživatele
No comment provided by engineer.
+
+ Error scanning code: %@
+ No comment provided by engineer.
+
Error sending email
Chyba odesílání e-mailu
@@ -2604,9 +2651,9 @@ This cannot be undone!
Členové skupin mohou přidávat reakce na zprávy.
No comment provided by engineer.
-
- Group members can irreversibly delete sent messages.
- Členové skupiny mohou nevratně mazat odeslané zprávy.
+
+ Group members can irreversibly delete sent messages. (24 hours)
+ Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)
No comment provided by engineer.
@@ -2714,6 +2761,10 @@ This cannot be undone!
Historie
No comment provided by engineer.
+
+ History is not sent to new members.
+ No comment provided by engineer.
+
How SimpleX works
Jak SimpleX funguje
@@ -2749,11 +2800,6 @@ This cannot be undone!
Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz.
No comment provided by engineer.
-
- If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.
- Pokud se nemůžete setkat osobně, můžete **naskenovat QR kód během videohovoru**, nebo váš kontakt může sdílet odkaz na pozvánku.
- No comment provided by engineer.
-
If you enter this passcode when opening the app, all app data will be irreversibly removed!
Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána!
@@ -2809,6 +2855,10 @@ This cannot be undone!
Import databáze
No comment provided by engineer.
+
+ Improved message delivery
+ No comment provided by engineer.
+
Improved privacy and security
Vylepšená ochrana soukromí a zabezpečení
@@ -2909,15 +2959,31 @@ This cannot be undone!
Rozhranní
No comment provided by engineer.
+
+ Invalid QR code
+ No comment provided by engineer.
+
Invalid connection link
Neplatný odkaz na spojení
No comment provided by engineer.
+
+ Invalid display name!
+ No comment provided by engineer.
+
+
+ Invalid link
+ No comment provided by engineer.
+
Invalid name!
No comment provided by engineer.
+
+ Invalid response
+ No comment provided by engineer.
+
Invalid server address!
Neplatná adresa serveru!
@@ -3009,6 +3075,10 @@ This cannot be undone!
Připojit ke skupině
No comment provided by engineer.
+
+ Join group conversations
+ No comment provided by engineer.
+
Join group?
No comment provided by engineer.
@@ -3032,10 +3102,18 @@ This is your link for group %@!
Připojování ke skupině
No comment provided by engineer.
+
+ Keep
+ No comment provided by engineer.
+
Keep the app open to use it from desktop
No comment provided by engineer.
+
+ Keep unused invitation?
+ No comment provided by engineer.
+
Keep your connections
Zachovat vaše připojení
@@ -3118,6 +3196,11 @@ This is your link for group %@!
Živé zprávy
No comment provided by engineer.
+
+ Local
+ Místní
+ No comment provided by engineer.
+
Local name
Místní název
@@ -3357,6 +3440,10 @@ This is your link for group %@!
Nové heslo
No comment provided by engineer.
+
+ New chat
+ No comment provided by engineer.
+
New contact request
Žádost o nový kontakt
@@ -3480,16 +3567,15 @@ This is your link for group %@!
- zakázat členy (role "pozorovatel")
No comment provided by engineer.
+
+ OK
+ No comment provided by engineer.
+
Off
Vypnout
No comment provided by engineer.
-
- Off (Local)
- Vypnuto (místní)
- No comment provided by engineer.
-
Ok
Ok
@@ -3550,9 +3636,9 @@ This is your link for group %@!
Reakce na zprávy můžete přidávat pouze vy.
No comment provided by engineer.
-
- Only you can irreversibly delete messages (your contact can mark them for deletion).
- Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání).
+
+ Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)
+ Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání). (24 hodin)
No comment provided by engineer.
@@ -3575,9 +3661,9 @@ This is your link for group %@!
Reakce na zprávy může přidávat pouze váš kontakt.
No comment provided by engineer.
-
- Only your contact can irreversibly delete messages (you can mark them for deletion).
- Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání).
+
+ Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)
+ Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání). (24 hodin)
No comment provided by engineer.
@@ -3629,9 +3715,16 @@ This is your link for group %@!
Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.
No comment provided by engineer.
-
- Opening database…
- Otvírání databáze…
+
+ Opening app…
+ No comment provided by engineer.
+
+
+ Or scan QR code
+ No comment provided by engineer.
+
+
+ Or show this code
No comment provided by engineer.
@@ -3674,10 +3767,9 @@ This is your link for group %@!
Heslo k zobrazení
No comment provided by engineer.
-
- Paste
- Vložit
- No comment provided by engineer.
+
+ Past member %@
+ past/unknown group member
Paste desktop address
@@ -3688,15 +3780,13 @@ This is your link for group %@!
Vložit obrázek
No comment provided by engineer.
-
- Paste received link
- Vložení přijatého odkazu
+
+ Paste link to connect!
No comment provided by engineer.
-
- Paste the link you received to connect with your contact.
- Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem.
- placeholder
+
+ Paste the link you received
+ No comment provided by engineer.
People can connect to you only via the links you share.
@@ -3733,6 +3823,11 @@ This is your link for group %@!
Zkontrolujte prosím nastavení své i svého kontaktu.
No comment provided by engineer.
+
+ Please contact developers.
+Error: %@
+ No comment provided by engineer.
+
Please contact group admin.
Kontaktujte prosím správce skupiny.
@@ -3818,6 +3913,10 @@ This is your link for group %@!
Soukromé názvy souborů
No comment provided by engineer.
+
+ Private notes
+ name of notes to self
+
Profile and server connections
Profil a připojení k serveru
@@ -3936,6 +4035,10 @@ This is your link for group %@!
Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).
No comment provided by engineer.
+
+ Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).
+ No comment provided by engineer.
+
Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).
Přečtěte si více v [Uživatelské příručce](https://simplex.chat/docs/guide/readme.html#connect-to-friends).
@@ -3991,6 +4094,10 @@ This is your link for group %@!
Příjem přes
No comment provided by engineer.
+
+ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).
+ No comment provided by engineer.
+
Recipients see updates as you type them.
Příjemci uvidí aktualizace během jejich psaní.
@@ -4144,6 +4251,10 @@ This is your link for group %@!
Chyba obnovení databáze
No comment provided by engineer.
+
+ Retry
+ No comment provided by engineer.
+
Reveal
Odhalit
@@ -4269,6 +4380,10 @@ This is your link for group %@!
Uložené servery WebRTC ICE budou odstraněny
No comment provided by engineer.
+
+ Saved message
+ message info title
+
Scan QR code
Skenovat QR kód
@@ -4298,6 +4413,14 @@ This is your link for group %@!
Hledat
No comment provided by engineer.
+
+ Search bar accepts invitation links.
+ No comment provided by engineer.
+
+
+ Search or paste SimpleX link
+ No comment provided by engineer.
+
Secure queue
Zabezpečit frontu
@@ -4403,6 +4526,10 @@ This is your link for group %@!
Odeslat je z galerie nebo vlastní klávesnice.
No comment provided by engineer.
+
+ Send up to 100 last messages to new members.
+ No comment provided by engineer.
+
Sender cancelled file transfer.
Odesílatel zrušil přenos souboru.
@@ -4572,9 +4699,8 @@ This is your link for group %@!
Sdílet odkaz
No comment provided by engineer.
-
- Share one-time invitation link
- Jednorázový zvací odkaz
+
+ Share this 1-time invite link
No comment provided by engineer.
@@ -4697,16 +4823,15 @@ This is your link for group %@!
Někdo
notification title
-
- Start a new chat
- Začít nový chat
- No comment provided by engineer.
-
Start chat
Začít chat
No comment provided by engineer.
+
+ Start chat?
+ No comment provided by engineer.
+
Start migration
Zahájit přenesení
@@ -4831,6 +4956,14 @@ This is your link for group %@!
Klepnutím se připojíte inkognito
No comment provided by engineer.
+
+ Tap to paste link
+ No comment provided by engineer.
+
+
+ Tap to scan
+ No comment provided by engineer.
+
Tap to start a new chat
Klepnutím na zahájíte nový chat
@@ -4893,6 +5026,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
Pokus o změnu přístupové fráze databáze nebyl dokončen.
No comment provided by engineer.
+
+ The code you scanned is not a SimpleX link QR code.
+ No comment provided by engineer.
+
The connection you accepted will be cancelled!
Připojení, které jste přijali, bude zrušeno!
@@ -4958,21 +5095,15 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
Servery pro nová připojení vašeho aktuálního chat profilu **%@**.
No comment provided by engineer.
+
+ The text you pasted is not a SimpleX link.
+ No comment provided by engineer.
+
Theme
Téma
No comment provided by engineer.
-
- There should be at least one user profile.
- Měl by tam být alespoň jeden uživatelský profil.
- No comment provided by engineer.
-
-
- There should be at least one visible user profile.
- Měl by tam být alespoň jeden viditelný uživatelský profil.
- No comment provided by engineer.
-
These settings are for your current profile **%@**.
Toto nastavení je pro váš aktuální profil **%@**.
@@ -5002,6 +5133,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
This device name
No comment provided by engineer.
+
+ This display name is invalid. Please choose another name.
+ No comment provided by engineer.
+
This group has over %lld members, delivery receipts are not sent.
Tato skupina má více než %lld členů, potvrzení o doručení nejsou odesílány.
@@ -5101,16 +5236,15 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.
Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.
No comment provided by engineer.
+
+ Turkish interface
+ No comment provided by engineer.
+
Turn off
Vypnout
No comment provided by engineer.
-
- Turn off notifications?
- Vypnout upozornění?
- No comment provided by engineer.
-
Turn on
Zapnout
@@ -5125,10 +5259,18 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.
Unblock
No comment provided by engineer.
+
+ Unblock for all
+ No comment provided by engineer.
+
Unblock member
No comment provided by engineer.
+
+ Unblock member for all?
+ No comment provided by engineer.
+
Unblock member?
No comment provided by engineer.
@@ -5223,6 +5365,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
Nepřečtený
No comment provided by engineer.
+
+ Up to 100 last messages are sent to new members.
+ No comment provided by engineer.
+
Update
Aktualizovat
@@ -5307,6 +5453,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
Použít nový inkognito profil
No comment provided by engineer.
+
+ Use only local notifications?
+ No comment provided by engineer.
+
Use server
Použít server
@@ -5383,6 +5533,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
Zobrazení bezpečnostního kódu
No comment provided by engineer.
+
+ Visible history
+ chat feature
+
Voice messages
Hlasové zprávy
@@ -5467,11 +5621,19 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve.
No comment provided by engineer.
+
+ With encrypted files and media.
+ No comment provided by engineer.
+
With optional welcome message.
S volitelnou uvítací zprávou.
No comment provided by engineer.
+
+ With reduced battery usage.
+ No comment provided by engineer.
+
Wrong database passphrase
Špatná přístupová fráze k databázi
@@ -5556,11 +5718,6 @@ Repeat join request?
Můžete přijímat hovory z obrazovky zámku, bez ověření zařízení a aplikace.
No comment provided by engineer.
-
- You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.
- Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko **Otevřít v mobilní aplikaci**.
- No comment provided by engineer.
-
You can create it later
Můžete vytvořit později
@@ -5581,6 +5738,10 @@ Repeat join request?
Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava.
No comment provided by engineer.
+
+ You can make it visible to your SimpleX contacts via Settings.
+ No comment provided by engineer.
+
You can now send messages to %@
Nyní můžete posílat zprávy %@
@@ -5621,6 +5782,10 @@ Repeat join request?
K formátování zpráv můžete použít markdown:
No comment provided by engineer.
+
+ You can view invitation link again in connection details.
+ No comment provided by engineer.
+
You can't send messages!
Nemůžete posílat zprávy!
@@ -5805,13 +5970,6 @@ Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s nov
Vaše kontakty mohou povolit úplné mazání zpráv.
No comment provided by engineer.
-
- Your contacts in SimpleX will see it.
-You can change it in Settings.
- Vaše kontakty v SimpleX ji uvidí.
-Můžete ji změnit v Nastavení.
- No comment provided by engineer.
-
Your contacts will remain connected.
Vaše kontakty zůstanou připojeny.
@@ -5960,6 +6118,14 @@ Servery SimpleX nevidí váš profil.
blocked
No comment provided by engineer.
+
+ blocked %@
+ rcv group event chat item
+
+
+ blocked by admin
+ blocked chat item
+
bold
tučně
@@ -6080,6 +6246,10 @@ Servery SimpleX nevidí váš profil.
připojení:%@
connection information
+
+ contact %1$@ changed to %2$@
+ profile update event chat item
+
contact has e2e encryption
kontakt má šifrování e2e
@@ -6348,6 +6518,10 @@ Servery SimpleX nevidí váš profil.
člen
member role
+
+ member %1$@ changed to %2$@
+ profile update event chat item
+
connected
připojeno
@@ -6470,6 +6644,14 @@ Servery SimpleX nevidí váš profil.
odstraněno %@
rcv group event chat item
+
+ removed contact address
+ profile update event chat item
+
+
+ removed profile picture
+ profile update event chat item
+
removed you
odstranil vás
@@ -6500,6 +6682,14 @@ Servery SimpleX nevidí váš profil.
odeslat přímou zprávu
No comment provided by engineer.
+
+ set new contact address
+ profile update event chat item
+
+
+ set new profile picture
+ profile update event chat item
+
starting…
začíná…
@@ -6515,16 +6705,28 @@ Servery SimpleX nevidí váš profil.
tento kontakt
notification title
+
+ unblocked %@
+ rcv group event chat item
+
unknown
neznámý
connection info
+
+ unknown status
+ No comment provided by engineer.
+
updated group profile
aktualizoval profil skupiny
rcv group event chat item
+
+ updated profile
+ profile update event chat item
+
v%@
No comment provided by engineer.
@@ -6594,6 +6796,10 @@ Servery SimpleX nevidí váš profil.
jste pozorovatel
No comment provided by engineer.
+
+ you blocked %@
+ snd group event chat item
+
you changed address
změnili jste adresu
@@ -6634,6 +6840,10 @@ Servery SimpleX nevidí váš profil.
sdíleli jste jednorázový odkaz inkognito
chat list item description
+
+ you unblocked %@
+ snd group event chat item
+
you:
vy:
diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
index 75f70f7ad1..d0f217a0e1 100644
--- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
+++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
@@ -49,7 +49,7 @@
## History
- ## Vergangenheit
+ ## Nachrichtenverlauf
copied message info
@@ -217,6 +217,11 @@
%lld Nachrichten blockiert
No comment provided by engineer.
+
+ %lld messages blocked by admin
+ %lld Nachrichten wurden vom Administrator blockiert
+ No comment provided by engineer.
+
%lld messages marked deleted
%lld Nachrichten als gelöscht markiert
@@ -312,14 +317,19 @@
)
No comment provided by engineer.
-
- **Add new contact**: to create your one-time QR Code or link for your contact.
- **Fügen Sie einen neuen Kontakt hinzu**: Erzeugen Sie einen Einmal-QR-Code oder -Link für Ihren Kontakt.
+
+ **Add contact**: to create a new invitation link, or connect via a link you received.
+ **Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen oder eine Verbindung über einen Link herzustellen, den Sie erhalten haben.
No comment provided by engineer.
-
- **Create link / QR code** for your contact to use.
- **Generieren Sie einen Einladungs-Link / QR code** für Ihren Kontakt.
+
+ **Add new contact**: to create your one-time QR Code or link for your contact.
+ **Neuen Kontakt hinzufügen**: Um einen Einmal-QR-Code oder -Link für Ihren Kontakt zu erzeugen.
+ No comment provided by engineer.
+
+
+ **Create group**: to create a new group.
+ **Gruppe erstellen**: Um eine neue Gruppe zu erstellen.
No comment provided by engineer.
@@ -332,11 +342,6 @@
**Beste Privatsphäre**: Es wird kein SimpleX-Chat-Benachrichtigungs-Server genutzt, Nachrichten werden in periodischen Abständen im Hintergrund geprüft (dies hängt davon ab, wie häufig Sie die App nutzen).
No comment provided by engineer.
-
- **Paste received link** or open it in the browser and tap **Open in mobile app**.
- **Fügen Sie den von Ihrem Kontakt erhaltenen Link ein** oder öffnen Sie ihn im Browser und tippen Sie auf **In mobiler App öffnen**.
- No comment provided by engineer.
-
**Please note**: you will NOT be able to recover or change passphrase if you lose it.
**Bitte beachten Sie**: Das Passwort kann NICHT wiederhergestellt oder geändert werden, wenn Sie es vergessen haben oder verlieren.
@@ -347,11 +352,6 @@
**Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde.
No comment provided by engineer.
-
- **Scan QR code**: to connect to your contact in person or via video call.
- **Scannen Sie den QR-Code**, um sich während einem persönlichen Treffen oder per Videoanruf mit Ihrem Kontakt zu verbinden.
- No comment provided by engineer.
-
**Warning**: Instant push notifications require passphrase saved in Keychain.
**Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist.
@@ -408,9 +408,9 @@
- voice messages up to 5 minutes.
- custom time to disappear.
- editing history.
- - Bis zu 5 Minuten lange Sprachnachrichten.
-- Zeitdauer für verschwindende Nachrichten anpassen.
-- Nachrichten-Historie bearbeiten.
+ - Bis zu 5 Minuten lange Sprachnachrichten
+- Zeitdauer für verschwindende Nachrichten anpassen
+- Nachrichtenverlauf bearbeiten
No comment provided by engineer.
@@ -453,11 +453,6 @@
wöchentlich
time interval
-
- 1-time link
- Einmal-Link
- No comment provided by engineer.
-
5 minutes
5 Minuten
@@ -519,12 +514,12 @@
Abort changing address
- Wechsel der Adresse abbrechen
+ Wechsel der Empfängeradresse abbrechen
No comment provided by engineer.
Abort changing address?
- Wechsel der Adresse abbrechen?
+ Wechsel der Empfängeradresse abbrechen?
No comment provided by engineer.
@@ -570,7 +565,12 @@
Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.
- Fügen Sie die Adresse zu Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.
+ Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.
+ No comment provided by engineer.
+
+
+ Add contact
+ Kontakt hinzufügen
No comment provided by engineer.
@@ -610,7 +610,7 @@
Address change will be aborted. Old receiving address will be used.
- Der Wechsel der Adresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet.
+ Der Wechsel der Empfängeradresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet.
No comment provided by engineer.
@@ -643,6 +643,11 @@
Alle Gruppenmitglieder bleiben verbunden.
No comment provided by engineer.
+
+ All messages will be deleted - this cannot be undone!
+ Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!
+ No comment provided by engineer.
+
All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.
Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.
@@ -650,7 +655,7 @@
All new messages from %@ will be hidden!
- Alle neuen Nachrichten von %@ werden verborgen!
+ Von %@ werden alle neuen Nachrichten ausgeblendet!
No comment provided by engineer.
@@ -675,12 +680,12 @@
Allow disappearing messages only if your contact allows it to you.
- Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.
+ Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.
No comment provided by engineer.
-
- Allow irreversible message deletion only if your contact allows it to you.
- Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.
+
+ Allow irreversible message deletion only if your contact allows it to you. (24 hours)
+ Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden)
No comment provided by engineer.
@@ -703,9 +708,9 @@
Das Senden von verschwindenden Nachrichten erlauben.
No comment provided by engineer.
-
- Allow to irreversibly delete sent messages.
- Unwiederbringliches löschen von gesendeten Nachrichten erlauben.
+
+ Allow to irreversibly delete sent messages. (24 hours)
+ Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden)
No comment provided by engineer.
@@ -738,9 +743,9 @@
Erlaubt Ihren Kontakten Sie anzurufen.
No comment provided by engineer.
-
- Allow your contacts to irreversibly delete sent messages.
- Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen.
+
+ Allow your contacts to irreversibly delete sent messages. (24 hours)
+ Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen. (24 Stunden)
No comment provided by engineer.
@@ -918,6 +923,11 @@
Blockieren
No comment provided by engineer.
+
+ Block for all
+ Für Alle blockieren
+ No comment provided by engineer.
+
Block group members
Gruppenmitglieder blockieren
@@ -928,19 +938,29 @@
Mitglied blockieren
No comment provided by engineer.
+
+ Block member for all?
+ Mitglied für Alle blockieren?
+ No comment provided by engineer.
+
Block member?
Mitglied blockieren?
No comment provided by engineer.
+
+ Blocked by admin
+ wurde vom Administrator blockiert
+ No comment provided by engineer.
+
Both you and your contact can add message reactions.
Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben.
No comment provided by engineer.
-
- Both you and your contact can irreversibly delete sent messages.
- Sowohl Ihr Kontakt, als auch Sie können gesendete Nachrichten unwiederbringlich löschen.
+
+ Both you and your contact can irreversibly delete sent messages. (24 hours)
+ Sowohl Ihr Kontakt, als auch Sie können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)
No comment provided by engineer.
@@ -978,9 +998,9 @@
Anrufe
No comment provided by engineer.
-
- Can't delete user profile!
- Das Benutzerprofil kann nicht gelöscht werden!
+
+ Camera not available
+ Kamera nicht verfügbar
No comment provided by engineer.
@@ -1094,6 +1114,11 @@
Der Chat ist beendet
No comment provided by engineer.
+
+ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.
+ Der Chat ist angehalten. Wenn Sie diese Datenbank bereits auf einem anderen Gerät genutzt haben, sollten Sie diese vor dem Starten des Chats wieder zurückspielen.
+ No comment provided by engineer.
+
Chat preferences
Chat-Präferenzen
@@ -1139,6 +1164,11 @@
Unterhaltung löschen?
No comment provided by engineer.
+
+ Clear private notes?
+ Private Notizen löschen?
+ No comment provided by engineer.
+
Clear verification
Überprüfung zurücknehmen
@@ -1217,7 +1247,7 @@
Connect to yourself?
This is your own SimpleX address!
- Mit Ihnen selbst verbinden?
+ Sich mit Ihnen selbst verbinden?
Das ist Ihre eigene SimpleX-Adresse!
No comment provided by engineer.
@@ -1238,11 +1268,6 @@ Das ist Ihr eigener Einmal-Link!
Über einen Link verbinden
No comment provided by engineer.
-
- Connect via link / QR code
- Über einen Link / QR-Code verbinden
- No comment provided by engineer.
-
Connect via one-time link
Über einen Einmal-Link verbinden
@@ -1418,11 +1443,6 @@ Das ist Ihr eigener Einmal-Link!
Neues Profil in der [Desktop-App] erstellen (https://simplex.chat/downloads/). 💻
No comment provided by engineer.
-
- Create one-time invitation link
- Einmal-Einladungslink erstellen
- No comment provided by engineer.
-
Create profile
Profil erstellen
@@ -1443,11 +1463,26 @@ Das ist Ihr eigener Einmal-Link!
Erstellen Sie Ihr Profil
No comment provided by engineer.
+
+ Created at
+ Erstellt um
+ No comment provided by engineer.
+
+
+ Created at: %@
+ Erstellt um: %@
+ copied message info
+
Created on %@
Erstellt am %@
No comment provided by engineer.
+
+ Creating link…
+ Link wird erstellt…
+ No comment provided by engineer.
+
Current Passcode
Aktueller Zugangscode
@@ -1875,7 +1910,7 @@ Das kann nicht rückgängig gemacht werden!
Disappearing messages
- verschwindende Nachrichten
+ Verschwindende Nachrichten
chat feature
@@ -1928,6 +1963,11 @@ Das kann nicht rückgängig gemacht werden!
Später wiederholen
No comment provided by engineer.
+
+ Do not send history to new members.
+ Den Nachrichtenverlauf nicht an neue Mitglieder senden.
+ No comment provided by engineer.
+
Don't create address
Keine Adresse erstellt
@@ -1998,6 +2038,11 @@ Das kann nicht rückgängig gemacht werden!
Automatisches Löschen von Nachrichten aktivieren?
No comment provided by engineer.
+
+ Enable camera access
+ Kamera-Zugriff aktivieren
+ No comment provided by engineer.
+
Enable for all
Für Alle aktivieren
@@ -2063,6 +2108,11 @@ Das kann nicht rückgängig gemacht werden!
Verschlüsselte Nachricht oder ein anderes Ereignis
notification
+
+ Encrypted message: app is stopped
+ Verschlüsselte Nachricht: Die App ist angehalten
+ notification
+
Encrypted message: database error
Verschlüsselte Nachricht: Datenbankfehler
@@ -2175,7 +2225,7 @@ Das kann nicht rückgängig gemacht werden!
Error changing address
- Fehler beim Wechseln der Adresse
+ Fehler beim Wechseln der Empfängeradresse
No comment provided by engineer.
@@ -2208,6 +2258,11 @@ Das kann nicht rückgängig gemacht werden!
Fehler beim Anlegen eines Mitglied-Kontaktes
No comment provided by engineer.
+
+ Error creating message
+ Fehler beim Erstellen der Nachricht
+ No comment provided by engineer.
+
Error creating profile!
Fehler beim Erstellen des Profils!
@@ -2293,6 +2348,11 @@ Das kann nicht rückgängig gemacht werden!
Fehler beim Laden von %@ Servern
No comment provided by engineer.
+
+ Error opening chat
+ Fehler beim Öffnen des Chats
+ No comment provided by engineer.
+
Error receiving file
Fehler beim Empfangen der Datei
@@ -2333,6 +2393,11 @@ Das kann nicht rückgängig gemacht werden!
Fehler beim Speichern des Benutzer-Passworts
No comment provided by engineer.
+
+ Error scanning code: %@
+ Fehler beim Scannen des Codes: %@
+ No comment provided by engineer.
+
Error sending email
Fehler beim Senden der eMail
@@ -2663,9 +2728,9 @@ Das kann nicht rückgängig gemacht werden!
Gruppenmitglieder können eine Reaktion auf Nachrichten geben.
No comment provided by engineer.
-
- Group members can irreversibly delete sent messages.
- Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen.
+
+ Group members can irreversibly delete sent messages. (24 hours)
+ Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)
No comment provided by engineer.
@@ -2770,7 +2835,12 @@ Das kann nicht rückgängig gemacht werden!
History
- Vergangenheit
+ Nachrichtenverlauf
+ No comment provided by engineer.
+
+
+ History is not sent to new members.
+ Der Nachrichtenverlauf wird nicht an neue Gruppenmitglieder gesendet.
No comment provided by engineer.
@@ -2808,11 +2878,6 @@ Das kann nicht rückgängig gemacht werden!
Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link.
No comment provided by engineer.
-
- If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.
- Wenn Sie sich nicht persönlich treffen können, kann der **QR-Code während eines Videoanrufs gescannt werden**, oder Ihr Kontakt kann den Einladungslink über einen anderen Kanal mit Ihnen teilen.
- No comment provided by engineer.
-
If you enter this passcode when opening the app, all app data will be irreversibly removed!
Wenn Sie diesen Zugangscode während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht!
@@ -2868,6 +2933,11 @@ Das kann nicht rückgängig gemacht werden!
Datenbank importieren
No comment provided by engineer.
+
+ Improved message delivery
+ Verbesserte Zustellung von Nachrichten
+ No comment provided by engineer.
+
Improved privacy and security
Verbesserte Privatsphäre und Sicherheit
@@ -2970,16 +3040,36 @@ Das kann nicht rückgängig gemacht werden!
Schnittstelle
No comment provided by engineer.
+
+ Invalid QR code
+ Ungültiger QR-Code
+ No comment provided by engineer.
+
Invalid connection link
Ungültiger Verbindungslink
No comment provided by engineer.
+
+ Invalid display name!
+ Ungültiger Anzeigename!
+ No comment provided by engineer.
+
+
+ Invalid link
+ Ungültiger Link
+ No comment provided by engineer.
+
Invalid name!
Ungültiger Name!
No comment provided by engineer.
+
+ Invalid response
+ Ungültige Reaktion
+ No comment provided by engineer.
+
Invalid server address!
Ungültige Serveradresse!
@@ -3071,6 +3161,11 @@ Das kann nicht rückgängig gemacht werden!
Treten Sie der Gruppe bei
No comment provided by engineer.
+
+ Join group conversations
+ Gruppenunterhaltungen beitreten
+ No comment provided by engineer.
+
Join group?
Der Gruppe beitreten?
@@ -3098,11 +3193,21 @@ Das ist Ihr Link für die Gruppe %@!
Der Gruppe beitreten
No comment provided by engineer.
+
+ Keep
+ Behalten
+ No comment provided by engineer.
+
Keep the app open to use it from desktop
Die App muss geöffnet bleiben, um sie vom Desktop aus nutzen zu können
No comment provided by engineer.
+
+ Keep unused invitation?
+ Nicht genutzte Einladung behalten?
+ No comment provided by engineer.
+
Keep your connections
Ihre Verbindungen beibehalten
@@ -3188,6 +3293,11 @@ Das ist Ihr Link für die Gruppe %@!
Live Nachrichten
No comment provided by engineer.
+
+ Local
+ Lokal
+ No comment provided by engineer.
+
Local name
Lokaler Name
@@ -3230,7 +3340,7 @@ Das ist Ihr Link für die Gruppe %@!
Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.
- Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.
+ Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.
No comment provided by engineer.
@@ -3428,6 +3538,11 @@ Das ist Ihr Link für die Gruppe %@!
Neuer Zugangscode
No comment provided by engineer.
+
+ New chat
+ Neuer Chat
+ No comment provided by engineer.
+
New contact request
Neue Kontaktanfrage
@@ -3515,7 +3630,7 @@ Das ist Ihr Link für die Gruppe %@!
No history
- Keine Vergangenheit
+ Kein Nachrichtenverlauf
No comment provided by engineer.
@@ -3552,16 +3667,16 @@ Das ist Ihr Link für die Gruppe %@!
- Gruppenmitglieder deaktivieren ("Beobachter"-Rolle)
No comment provided by engineer.
+
+ OK
+ OK
+ No comment provided by engineer.
+
Off
Aus
No comment provided by engineer.
-
- Off (Local)
- Aus (Lokal)
- No comment provided by engineer.
-
Ok
Ok
@@ -3622,9 +3737,9 @@ Das ist Ihr Link für die Gruppe %@!
Nur Sie können Reaktionen auf Nachrichten geben.
No comment provided by engineer.
-
- Only you can irreversibly delete messages (your contact can mark them for deletion).
- Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren).
+
+ Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)
+ Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren). (24 Stunden)
No comment provided by engineer.
@@ -3647,9 +3762,9 @@ Das ist Ihr Link für die Gruppe %@!
Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben.
No comment provided by engineer.
-
- Only your contact can irreversibly delete messages (you can mark them for deletion).
- Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).
+
+ Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)
+ Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren). (24 Stunden)
No comment provided by engineer.
@@ -3702,9 +3817,19 @@ Das ist Ihr Link für die Gruppe %@!
Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen.
No comment provided by engineer.
-
- Opening database…
- Öffne Datenbank …
+
+ Opening app…
+ App wird geöffnet…
+ No comment provided by engineer.
+
+
+ Or scan QR code
+ Oder den QR-Code scannen
+ No comment provided by engineer.
+
+
+ Or show this code
+ Oder diesen QR-Code anzeigen
No comment provided by engineer.
@@ -3747,10 +3872,10 @@ Das ist Ihr Link für die Gruppe %@!
Passwort anzeigen
No comment provided by engineer.
-
- Paste
- Einfügen
- No comment provided by engineer.
+
+ Past member %@
+ Ehemaliges Mitglied %@
+ past/unknown group member
Paste desktop address
@@ -3762,15 +3887,15 @@ Das ist Ihr Link für die Gruppe %@!
Bild einfügen
No comment provided by engineer.
-
- Paste received link
- Fügen Sie den erhaltenen Link ein
+
+ Paste link to connect!
+ Zum Verbinden den Link einfügen!
No comment provided by engineer.
-
- Paste the link you received to connect with your contact.
- Um sich mit Ihrem Kontakt zu verbinden, fügen Sie den erhaltenen Link in das Feld unten ein.
- placeholder
+
+ Paste the link you received
+ Fügen Sie den erhaltenen Link ein
+ No comment provided by engineer.
People can connect to you only via the links you share.
@@ -3807,6 +3932,13 @@ Das ist Ihr Link für die Gruppe %@!
Bitte überprüfen sie sowohl Ihre, als auch die Präferenzen Ihres Kontakts.
No comment provided by engineer.
+
+ Please contact developers.
+Error: %@
+ Bitte nehmen Sie Kontakt mit den Entwicklern auf.
+Fehler: %@
+ No comment provided by engineer.
+
Please contact group admin.
Bitte kontaktieren Sie den Gruppen-Administrator.
@@ -3892,6 +4024,11 @@ Das ist Ihr Link für die Gruppe %@!
Neutrale Dateinamen
No comment provided by engineer.
+
+ Private notes
+ Private Notizen
+ name of notes to self
+
Profile and server connections
Profil und Serververbindungen
@@ -4012,6 +4149,11 @@ Das ist Ihr Link für die Gruppe %@!
Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) lesen.
No comment provided by engineer.
+
+ Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).
+ Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).
+ No comment provided by engineer.
+
Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).
Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/readme.html#connect-to-friends) lesen.
@@ -4067,6 +4209,11 @@ Das ist Ihr Link für die Gruppe %@!
Empfangen über
No comment provided by engineer.
+
+ Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).
+ Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).
+ No comment provided by engineer.
+
Recipients see updates as you type them.
Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.
@@ -4222,6 +4369,11 @@ Das ist Ihr Link für die Gruppe %@!
Fehler bei der Wiederherstellung der Datenbank
No comment provided by engineer.
+
+ Retry
+ Wiederholen
+ No comment provided by engineer.
+
Reveal
Aufdecken
@@ -4347,6 +4499,11 @@ Das ist Ihr Link für die Gruppe %@!
Gespeicherte WebRTC ICE-Server werden entfernt
No comment provided by engineer.
+
+ Saved message
+ Gespeicherte Nachricht
+ message info title
+
Scan QR code
QR-Code scannen
@@ -4377,6 +4534,16 @@ Das ist Ihr Link für die Gruppe %@!
Suche
No comment provided by engineer.
+
+ Search bar accepts invitation links.
+ Von der Suchleiste werden Einladungslinks akzeptiert.
+ No comment provided by engineer.
+
+
+ Search or paste SimpleX link
+ Suchen oder fügen Sie den SimpleX-Link ein
+ No comment provided by engineer.
+
Secure queue
Sichere Warteschlange
@@ -4482,6 +4649,11 @@ Das ist Ihr Link für die Gruppe %@!
Senden Sie diese aus dem Fotoalbum oder von individuellen Tastaturen.
No comment provided by engineer.
+
+ Send up to 100 last messages to new members.
+ Bis zu 100 der letzten Nachrichten an neue Gruppenmitglieder senden.
+ No comment provided by engineer.
+
Sender cancelled file transfer.
Der Absender hat die Dateiübertragung abgebrochen.
@@ -4652,9 +4824,9 @@ Das ist Ihr Link für die Gruppe %@!
Link teilen
No comment provided by engineer.
-
- Share one-time invitation link
- Einmal-Einladungslink teilen
+
+ Share this 1-time invite link
+ Teilen Sie diesen Einmal-Einladungslink
No comment provided by engineer.
@@ -4777,16 +4949,16 @@ Das ist Ihr Link für die Gruppe %@!
Jemand
notification title
-
- Start a new chat
- Starten Sie einen neuen Chat
- No comment provided by engineer.
-
Start chat
Starten Sie den Chat
No comment provided by engineer.
+
+ Start chat?
+ Chat starten?
+ No comment provided by engineer.
+
Start migration
Starten Sie die Migration
@@ -4894,12 +5066,12 @@ Das ist Ihr Link für die Gruppe %@!
Tap to Connect
- Zum Verbinden antippen
+ Zum Verbinden tippen
No comment provided by engineer.
Tap to activate profile.
- Tippen Sie auf das Profil um es zu aktivieren.
+ Zum Aktivieren des Profils tippen.
No comment provided by engineer.
@@ -4909,12 +5081,22 @@ Das ist Ihr Link für die Gruppe %@!
Tap to join incognito
- Tippen, um Inkognito beizutreten
+ Zum Inkognito beitreten tippen
+ No comment provided by engineer.
+
+
+ Tap to paste link
+ Zum Link einfügen tippen
+ No comment provided by engineer.
+
+
+ Tap to scan
+ Zum Scannen tippen
No comment provided by engineer.
Tap to start a new chat
- Tippen, um einen neuen Chat zu starten
+ Zum Starten eines neuen Chats tippen
No comment provided by engineer.
@@ -4974,6 +5156,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
Die Änderung des Datenbank-Passworts konnte nicht abgeschlossen werden.
No comment provided by engineer.
+
+ The code you scanned is not a SimpleX link QR code.
+ Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code.
+ No comment provided by engineer.
+
The connection you accepted will be cancelled!
Die von Ihnen akzeptierte Verbindung wird abgebrochen!
@@ -5036,7 +5223,12 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
The servers for new connections of your current chat profile **%@**.
- Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil **%@**.
+ Mögliche Server für neue Verbindungen von Ihrem aktuellen Chat-Profil **%@**.
+ No comment provided by engineer.
+
+
+ The text you pasted is not a SimpleX link.
+ Der von Ihnen eingefügte Text ist kein SimpleX-Link.
No comment provided by engineer.
@@ -5044,16 +5236,6 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
Design
No comment provided by engineer.
-
- There should be at least one user profile.
- Es muss mindestens ein Benutzer-Profil vorhanden sein.
- No comment provided by engineer.
-
-
- There should be at least one visible user profile.
- Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein.
- No comment provided by engineer.
-
These settings are for your current profile **%@**.
Diese Einstellungen betreffen Ihr aktuelles Profil **%@**.
@@ -5084,6 +5266,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
Dieser Gerätename
No comment provided by engineer.
+
+ This display name is invalid. Please choose another name.
+ Der Anzeigename ist ungültig. Bitte wählen Sie einen anderen Namen.
+ No comment provided by engineer.
+
This group has over %lld members, delivery receipts are not sent.
Es werden keine Empfangsbestätigungen gesendet, da diese Gruppe über %lld Mitglieder hat.
@@ -5186,16 +5373,16 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.
No comment provided by engineer.
+
+ Turkish interface
+ Türkische Bedienoberfläche
+ No comment provided by engineer.
+
Turn off
Abschalten
No comment provided by engineer.
-
- Turn off notifications?
- Benachrichtigungen abschalten?
- No comment provided by engineer.
-
Turn on
Einschalten
@@ -5211,11 +5398,21 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
Freigeben
No comment provided by engineer.
+
+ Unblock for all
+ Für Alle freigeben
+ No comment provided by engineer.
+
Unblock member
Mitglied freigeben
No comment provided by engineer.
+
+ Unblock member for all?
+ Mitglied für Alle freigeben?
+ No comment provided by engineer.
+
Unblock member?
Mitglied freigeben?
@@ -5313,6 +5510,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Ungelesen
No comment provided by engineer.
+
+ Up to 100 last messages are sent to new members.
+ Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet.
+ No comment provided by engineer.
+
Update
Aktualisieren
@@ -5398,6 +5600,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Nutzen Sie das neue Inkognito-Profil
No comment provided by engineer.
+
+ Use only local notifications?
+ Nur lokale Benachrichtigungen nutzen?
+ No comment provided by engineer.
+
Use server
Server nutzen
@@ -5478,6 +5685,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Schauen Sie sich den Sicherheitscode an
No comment provided by engineer.
+
+ Visible history
+ Sichtbarer Nachrichtenverlauf
+ chat feature
+
Voice messages
Sprachnachrichten
@@ -5563,11 +5775,21 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden.
No comment provided by engineer.
+
+ With encrypted files and media.
+ Mit verschlüsselten Dateien und Medien.
+ No comment provided by engineer.
+
With optional welcome message.
Mit optionaler Begrüßungsmeldung.
No comment provided by engineer.
+
+ With reduced battery usage.
+ Mit reduziertem Akkuverbrauch.
+ No comment provided by engineer.
+
Wrong database passphrase
Falsches Datenbank-Passwort
@@ -5585,7 +5807,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
You
- Ihre Daten
+ Profil
No comment provided by engineer.
@@ -5660,11 +5882,6 @@ Verbindungsanfrage wiederholen?
Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen.
No comment provided by engineer.
-
- You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.
- Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche **In mobiler App öffnen**.
- No comment provided by engineer.
-
You can create it later
Sie können dies später erstellen
@@ -5685,6 +5902,11 @@ Verbindungsanfrage wiederholen?
Sie können ein Benutzerprofil verbergen oder stummschalten - wischen Sie es nach rechts.
No comment provided by engineer.
+
+ You can make it visible to your SimpleX contacts via Settings.
+ Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen.
+ No comment provided by engineer.
+
You can now send messages to %@
Sie können nun Nachrichten an %@ versenden
@@ -5725,6 +5947,11 @@ Verbindungsanfrage wiederholen?
Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:
No comment provided by engineer.
+
+ You can view invitation link again in connection details.
+ Den Einladungslink können Sie in den Details der Verbindung nochmals sehen.
+ No comment provided by engineer.
+
You can't send messages!
Sie können keine Nachrichten versenden!
@@ -5834,12 +6061,12 @@ Verbindungsanfrage wiederholen?
You will stop receiving messages from this group. Chat history will be preserved.
- Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.
+ Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten.
No comment provided by engineer.
You won't lose your contacts if you later delete your address.
- Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.
+ Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.
No comment provided by engineer.
@@ -5914,13 +6141,6 @@ Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später
Ihre Kontakte können die unwiederbringliche Löschung von Nachrichten erlauben.
No comment provided by engineer.
-
- Your contacts in SimpleX will see it.
-You can change it in Settings.
- Ihre Kontakte in SimpleX werden es sehen.
-Sie können es in den Einstellungen ändern.
- No comment provided by engineer.
-
Your contacts will remain connected.
Ihre Kontakte bleiben verbunden.
@@ -5985,7 +6205,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.
Your settings
- Ihre Einstellungen
+ Einstellungen
No comment provided by engineer.
@@ -6070,9 +6290,19 @@ SimpleX-Server können Ihr Profil nicht einsehen.
blocked
- blockiert
+ Blockiert
No comment provided by engineer.
+
+ blocked %@
+ %@ wurde blockiert
+ rcv group event chat item
+
+
+ blocked by admin
+ wurde vom Administrator blockiert
+ blocked chat item
+
bold
fett
@@ -6100,7 +6330,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.
changed address for you
- wechselte die Adresse für Sie
+ Wechselte die Empfängeradresse von Ihnen
chat item text
@@ -6115,12 +6345,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.
changing address for %@…
- Adresse von %@ wechseln…
+ Empfängeradresse für %@ wechseln wird gestartet…
chat item text
changing address…
- Wechsel der Adresse…
+ Wechsel der Empfängeradresse wurde gestartet…
chat item text
@@ -6193,6 +6423,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.
Verbindung:%@
connection information
+
+ contact %1$@ changed to %2$@
+ Der Kontaktname %1$@ wurde auf %2$@ geändert
+ profile update event chat item
+
contact has e2e encryption
Kontakt nutzt E2E-Verschlüsselung
@@ -6463,6 +6698,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.
Mitglied
member role
+
+ member %1$@ changed to %2$@
+ Der Mitgliedsname %1$@ wurde auf %2$@ geändert
+ profile update event chat item
+
connected
ist der Gruppe beigetreten
@@ -6585,6 +6825,16 @@ SimpleX-Server können Ihr Profil nicht einsehen.
hat %@ aus der Gruppe entfernt
rcv group event chat item
+
+ removed contact address
+ Kontaktadresse wurde entfernt
+ profile update event chat item
+
+
+ removed profile picture
+ Profil-Bild wurde entfernt
+ profile update event chat item
+
removed you
hat Sie aus der Gruppe entfernt
@@ -6615,6 +6865,16 @@ SimpleX-Server können Ihr Profil nicht einsehen.
Direktnachricht senden
No comment provided by engineer.
+
+ set new contact address
+ Neue Kontaktadresse wurde festgelegt
+ profile update event chat item
+
+
+ set new profile picture
+ Neues Profil-Bild wurde festgelegt
+ profile update event chat item
+
starting…
Verbindung wird gestartet…
@@ -6630,16 +6890,31 @@ SimpleX-Server können Ihr Profil nicht einsehen.
Dieser Kontakt
notification title
+
+ unblocked %@
+ %@ wurde freigegeben
+ rcv group event chat item
+
unknown
Unbekannt
connection info
+
+ unknown status
+ unbekannter Gruppenmitglieds-Status
+ No comment provided by engineer.
+
updated group profile
Aktualisiertes Gruppenprofil
rcv group event chat item
+
+ updated profile
+ Das Profil wurde aktualisiert
+ profile update event chat item
+
v%@
v%@
@@ -6710,14 +6985,19 @@ SimpleX-Server können Ihr Profil nicht einsehen.
Sie sind Beobachter
No comment provided by engineer.
+
+ you blocked %@
+ Sie haben %@ blockiert
+ snd group event chat item
+
you changed address
- Sie haben die Adresse gewechselt
+ Die Empfängeradresse wurde gewechselt
chat item text
you changed address for %@
- Sie haben die Adresse für %@ gewechselt
+ Die Empfängeradresse für %@ wurde gewechselt
chat item text
@@ -6750,6 +7030,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.
Sie haben Inkognito einen Einmal-Link geteilt
chat list item description
+
+ you unblocked %@
+ Sie haben %@ freigegeben
+ snd group event chat item
+
you:
Sie:
diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff
index 7649b595cd..18051ae350 100644
--- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff
+++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff
@@ -31,48 +31,59 @@ Available in v5.1
(
No comment provided by engineer.
-
+
(can be copied)
+ (μπορεί να αντιγραφή)
No comment provided by engineer.
-
+
!1 colored!
+ !1 έγχρωμο!
No comment provided by engineer.
-
+
#secret#
+ #μυστικό#
No comment provided by engineer.
-
+
%@
+ %@
No comment provided by engineer.
-
+
%@ %@
+ %@ %@
No comment provided by engineer.
-
+
%@ / %@
+ %@ / %@
No comment provided by engineer.
-
+
%@ is connected!
+ %@ είναι συνδεδεμένο!
notification title
-
+
%@ is not verified
+ %@ δεν είναι επαληθευμένο
No comment provided by engineer.
-
+
%@ is verified
+ %@ είναι επαληθευμένο
No comment provided by engineer.
-
+
%@ servers
+ %@ διακομιστές
No comment provided by engineer.
-
+
%@ wants to connect!
+ %@ θέλει να συνδεθεί!
notification title
@@ -4162,6 +4173,66 @@ SimpleX servers cannot see your profile.
\~strike~
No comment provided by engineer.
+
+ %@ connected
+ %@ συνδεδεμένο
+ No comment provided by engineer.
+
+
+ # %@
+ # %@
+ copied message info title, # <title>
+
+
+ %@ and %@
+ %@ και %@
+ No comment provided by engineer.
+
+
+ %1$@ at %2$@:
+ %1$@ στις %2$@:
+ copied message info, <sender> at <time>
+
+
+ ## History
+ ## Ιστορικό
+ copied message info
+
+
+ ## In reply to
+ ## Ως απαντηση σε
+ copied message info
+
+
+ %@ (current)
+ %@ (τωρινό)
+ No comment provided by engineer.
+
+
+ %@ (current):
+ %@ (τωρινό):
+ copied message info
+
+
+ %@ and %@ connected
+ %@ και %@ συνδεδεμένο
+ No comment provided by engineer.
+
+
+ %@:
+ %@:
+ copied message info
+
+
+ %@, %@ and %lld members
+ %@, %@ και %lld μέλη
+ No comment provided by engineer.
+
+
+ %@, %@ and %lld other members connected
+ %@, %@ και %lld άλλα μέλη συνδέθηκαν
+ No comment provided by engineer.
+