Compare commits

..

1 Commits

Author SHA1 Message Date
Avently 391892bd64 ui: chatitem header 2023-11-13 10:29:22 -08:00
464 changed files with 11593 additions and 35059 deletions
+32 -69
View File
@@ -42,7 +42,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
name: build-${{ matrix.os }}-${{ matrix.ghc }}
name: build-${{ matrix.os }}
if: always()
needs: prepare-release
runs-on: ${{ matrix.os }}
@@ -51,25 +51,18 @@ 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
@@ -88,17 +81,16 @@ jobs:
- name: Setup Haskell
uses: haskell-actions/setup@v2
with:
ghc-version: ${{ matrix.ghc }}
ghc-version: "9.6.3"
cabal-version: "3.10.1.0"
- name: Restore cached build
id: restore_cache
uses: actions/cache/restore@v3
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
${{ matrix.cache_path }}
dist-newstyle
key: ${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
# / Unix
@@ -113,7 +105,7 @@ jobs:
echo " flags: +openssl" >> cabal.project.local
- name: Install AppImage dependencies
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
run: sudo apt install -y desktop-file-utils
- name: Install pkg-config for Mac
@@ -139,7 +131,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.asset_name && matrix.os != 'windows-latest'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -148,7 +140,7 @@ jobs:
tag: ${{ github.ref }}
- name: Unix update CLI binary hash
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os != 'windows-latest'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -158,7 +150,7 @@ jobs:
${{ steps.unix_cli_build.outputs.bin_hash }}
- name: Setup Java
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/setup-java@v3
with:
distribution: 'corretto'
@@ -167,7 +159,7 @@ jobs:
- name: Linux build desktop
id: linux_desktop_build
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
shell: bash
run: |
scripts/desktop/build-lib-linux.sh
@@ -176,10 +168,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.asset_name && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
shell: bash
run: |
scripts/desktop/make-appimage-linux.sh
@@ -202,7 +194,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.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -211,7 +203,7 @@ jobs:
tag: ${{ github.ref }}
- name: Linux update desktop package hash
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04')
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -221,7 +213,7 @@ jobs:
${{ steps.linux_desktop_build.outputs.package_hash }}
- name: Linux upload AppImage to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -230,7 +222,7 @@ jobs:
tag: ${{ github.ref }}
- name: Linux update AppImage hash
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -258,15 +250,6 @@ 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
@@ -278,36 +261,11 @@ jobs:
# / Windows
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
- name: 'Setup MSYS2'
if: matrix.os == 'windows-latest'
uses: msys2/setup-msys2@v2
with:
msystem: ucrt64
update: true
install: >-
git
perl
make
pacboy: >-
toolchain:p
cmake:p
- name: Windows build
id: windows_build
if: matrix.os == 'windows-latest'
shell: msys2 {0}
shell: bash
run: |
export PATH=$PATH:/c/ghcup/bin
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
echo "ignore-project: False" >> cabal.project.local
echo "package direct-sqlcipher" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local
echo " extra-include-dirs: $openssl_windows_style_path\include" >> cabal.project.local
echo " extra-lib-dirs: $openssl_windows_style_path" >> cabal.project.local
rm -rf dist-newstyle/src/direct-sq*
sed -i "s/, unix /--, unix /" simplex-chat.cabal
cabal build --enable-tests
@@ -335,6 +293,20 @@ jobs:
body: |
${{ steps.windows_build.outputs.bin_hash }}
- name: 'Setup MSYS2'
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
uses: msys2/setup-msys2@v2
with:
msystem: ucrt64
update: true
install: >-
git
perl
make
pacboy: >-
toolchain:p
cmake:p
- name: Windows build desktop
id: windows_desktop_build
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
@@ -347,7 +319,7 @@ jobs:
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
@@ -367,13 +339,4 @@ 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 /
+4 -6
View File
@@ -232,8 +232,6 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates:
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
@@ -368,13 +366,13 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Message delivery confirmation (with sender opt-out per contact).
- ✅ Desktop client.
- ✅ Encryption of local files stored in the app.
- Using mobile profiles from the desktop app.
- 🏗 Improve experience for the new users.
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
- 🏗 Large groups, communities and public channels.
- 🏗 Using mobile profiles from the desktop app.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- Post-quantum resistant key exchange in double ratchet protocol.
- Large groups, communities and public channels.
- Privacy & security slider - a simple way to set all settings at once.
- Improve sending videos (including encryption of locally stored videos).
- Improve experience for the new users.
- 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.).
+1 -7
View File
@@ -15,7 +15,6 @@ 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
}
@@ -37,17 +36,12 @@ 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)")
let m = ChatModel.shared
let deviceToken = DeviceToken(pushProvider: PushProvider(env: pushEnvironment), token: token)
m.deviceToken = deviceToken
// savedToken is set in startChat, when it is started before this method is called
if m.savedToken != nil {
registerToken(token: deviceToken)
}
@@ -86,7 +80,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
} else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages {
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages")
if m.ntfEnablePeriodic && allowBackgroundRefresh() && BGManager.shared.lastRanLongAgo {
if appStateGroupDefault.get().inactive && m.ntfEnablePeriodic {
receiveMessages(completionHandler)
} else {
completionHandler(.noData)
+61 -96
View File
@@ -14,14 +14,11 @@ struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@Environment(\.colorScheme) var colorScheme
var contentAccessAuthenticationExtended: Bool
@Environment(\.scenePhase) var scenePhase
@State private var automaticAuthenticationAttempted = false
@State private var canConnectViewCall = false
@State private var lastSuccessfulUnlock: TimeInterval? = nil
@Binding var doAuthenticate: Bool
@Binding var userAuthorized: Bool?
@Binding var canConnectCall: Bool
@Binding var lastSuccessfulUnlock: TimeInterval?
@Binding var showInitializationView: Bool
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -43,19 +40,9 @@ struct ContentView: View {
}
}
private var accessAuthenticated: Bool {
chatModel.contentViewAccessAuthenticated || contentAccessAuthenticationExtended
}
var body: some View {
ZStack {
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
// i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
if !prefPerformLA || accessAuthenticated {
contentView()
} else {
lockButton()
}
contentView()
if chatModel.showCallView, let call = chatModel.activeCall {
callView(call)
}
@@ -63,7 +50,6 @@ struct ContentView: View {
LocalAuthView(authRequest: la)
} else if showSetPasscode {
SetAppPasscodeView {
chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
showSetPasscode = false
privacyLocalAuthModeDefault.set(.passcode)
@@ -74,9 +60,13 @@ struct ContentView: View {
alertManager.showAlert(laPasscodeNotSetAlert())
}
}
if chatModel.chatDbStatus == nil {
initializationView()
}
}
.onAppear {
if prefPerformLA { requestNtfAuthorization() }
initAuthenticate()
}
.onChange(of: doAuthenticate) { _ in
initAuthenticate()
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
@@ -86,44 +76,14 @@ struct ContentView: View {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
// also see .onChange(of: scenePhase) in SimpleXApp: on entering background
// it remembers enteredBackgroundAuthenticated and sets chatModel.contentViewAccessAuthenticated to false
automaticAuthenticationAttempted = false
canConnectViewCall = false
case .active:
canConnectViewCall = !prefPerformLA || contentAccessAuthenticationExtended || unlockedRecently()
// condition `!chatModel.contentViewAccessAuthenticated` is required for when authentication is enabled in settings or on initial notice
if prefPerformLA && !chatModel.contentViewAccessAuthenticated {
if AppChatState.shared.value != .stopped {
if contentAccessAuthenticationExtended {
chatModel.contentViewAccessAuthenticated = true
} else {
if !automaticAuthenticationAttempted {
automaticAuthenticationAttempted = true
// authenticate if call kit call is not in progress
if !(CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil) {
authenticateContentViewAccess()
}
}
}
} else {
// when app is stopped automatic authentication is not attempted
chatModel.contentViewAccessAuthenticated = contentAccessAuthenticationExtended
}
}
default:
break
}
}
}
@ViewBuilder private func contentView() -> some View {
if let status = chatModel.chatDbStatus, status != .ok {
if prefPerformLA && userAuthorized != true {
lockButton()
} else if chatModel.chatDbStatus == nil && showInitializationView {
initializationView()
} else if let status = chatModel.chatDbStatus, status != .ok {
DatabaseErrorView(status: status)
} else if !chatModel.v3DBMigration.startChat {
MigrateToAppGroupView()
@@ -146,11 +106,11 @@ struct ContentView: View {
if CallController.useCallKit() {
ActiveCallView(call: call, canConnectCall: Binding.constant(true))
.onDisappear {
if prefPerformLA && !accessAuthenticated { authenticateContentViewAccess() }
if userAuthorized == false && doAuthenticate { runAuthenticate() }
}
} else {
ActiveCallView(call: call, canConnectCall: $canConnectViewCall)
if prefPerformLA && !accessAuthenticated {
ActiveCallView(call: call, canConnectCall: $canConnectCall)
if prefPerformLA && userAuthorized != true {
Rectangle()
.fill(colorScheme == .dark ? .black : .white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -160,27 +120,22 @@ struct ContentView: View {
}
private func lockButton() -> some View {
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
}
private func initializationView() -> some View {
VStack {
ProgressView().scaleEffect(2)
Text("Opening app")
Text("Opening database")
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity )
.background(
Rectangle()
.fill(.background)
)
}
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
.onAppear {
requestNtfAuthorization()
if !prefPerformLA { requestNtfAuthorization() }
// Local Authentication notice is to be shown on next start after onboarding is complete
if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
prefLANoticeShown = true
@@ -232,37 +187,48 @@ struct ContentView: View {
}
}
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
return false
private func initAuthenticate() {
logger.debug("initAuthenticate")
if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
userAuthorized = false
} else if doAuthenticate {
runAuthenticate()
}
}
private func authenticateContentViewAccess() {
logger.debug("DEBUGGING: authenticateContentViewAccess")
dismissAllSheets(animated: false) {
logger.debug("DEBUGGING: authenticateContentViewAccess, in dismissAllSheets callback")
chatModel.chatId = nil
private func runAuthenticate() {
logger.debug("DEBUGGING: runAuthenticate")
if !prefPerformLA {
userAuthorized = true
} else {
logger.debug("DEBUGGING: before dismissAllSheets")
dismissAllSheets(animated: false) {
logger.debug("DEBUGGING: in dismissAllSheets callback")
chatModel.chatId = nil
justAuthenticate()
}
}
}
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:
chatModel.contentViewAccessAuthenticated = true
canConnectViewCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
chatModel.contentViewAccessAuthenticated = false
if privacyLocalAuthModeDefault.get() == .passcode {
AlertManager.shared.showAlert(laFailedAlert())
}
case .unavailable:
prefPerformLA = false
canConnectViewCall = true
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
private func justAuthenticate() {
userAuthorized = false
let laMode = privacyLocalAuthModeDefault.get()
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:
userAuthorized = true
canConnectCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
if laMode == .passcode {
AlertManager.shared.showAlert(laFailedAlert())
}
case .unavailable:
userAuthorized = true
prefPerformLA = false
canConnectCall = true
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
}
}
@@ -293,7 +259,6 @@ struct ContentView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
alertManager.showAlert(laTurnedOnAlert())
case .failed:
+6 -28
View File
@@ -15,13 +15,7 @@ private let receiveTaskId = "chat.simplex.app.receive"
// TCP timeout + 2 sec
private let waitForMessages: TimeInterval = 6
// This is the smallest interval between refreshes, and also target interval in "off" mode
private let bgRefreshInterval: TimeInterval = 600 // 10 minutes
// This intervals are used for background refresh in instant and periodic modes
private let periodicBgRefreshInterval: TimeInterval = 1200 // 20 minutes
private let maxBgRefreshInterval: TimeInterval = 2400 // 40 minutes
private let bgRefreshInterval: TimeInterval = 450
private let maxTimerCount = 9
@@ -39,14 +33,14 @@ class BGManager {
}
}
func schedule(interval: TimeInterval? = nil) {
func schedule() {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.schedule: disabled")
return
}
logger.debug("BGManager.schedule")
let request = BGAppRefreshTaskRequest(identifier: receiveTaskId)
request.earliestBeginDate = Date(timeIntervalSinceNow: interval ?? runInterval)
request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshInterval)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
@@ -54,34 +48,20 @@ class BGManager {
}
}
var runInterval: TimeInterval {
switch ChatModel.shared.notificationMode {
case .instant: maxBgRefreshInterval
case .periodic: periodicBgRefreshInterval
case .off: bgRefreshInterval
}
}
var lastRanLongAgo: Bool {
Date.now.timeIntervalSince(chatLastBackgroundRunGroupDefault.get()) > runInterval
}
private func handleRefresh(_ task: BGAppRefreshTask) {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.handleRefresh: disabled")
return
}
logger.debug("BGManager.handleRefresh")
let shouldRun_ = lastRanLongAgo
if allowBackgroundRefresh() && shouldRun_ {
schedule()
schedule()
if appStateGroupDefault.get().inactive {
let completeRefresh = completionHandler {
task.setTaskCompleted(success: true)
}
task.expirationHandler = { completeRefresh("expirationHandler") }
receiveMessages(completeRefresh)
} else {
schedule(interval: shouldRun_ ? bgRefreshInterval : runInterval)
logger.debug("BGManager.completionHandler: already active, not started")
task.setTaskCompleted(success: true)
}
@@ -110,22 +90,20 @@ class BGManager {
}
self.completed = false
DispatchQueue.main.async {
chatLastBackgroundRunGroupDefault.set(Date.now)
let m = ChatModel.shared
if (!m.chatInitialized) {
setAppState(.bgRefresh)
do {
try initializeChat(start: true)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
}
activateChat(appState: .bgRefresh)
if m.currentUser == nil {
completeReceiving("no current user")
return
}
logger.debug("BGManager.receiveMessages: starting chat")
activateChat(appState: .bgRefresh)
let cr = ChatReceiver()
self.chatReceiver = cr
cr.start()
+12 -74
View File
@@ -54,8 +54,6 @@ final class ChatModel: ObservableObject {
@Published var chatDbChanged = false
@Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult?
// local authentication
@Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest?
// list of chat "previews"
@Published var chats: [Chat] = []
@@ -85,19 +83,16 @@ final class ChatModel: ObservableObject {
// current WebRTC call
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
@Published var activeCall: Call?
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
@Published var callCommand: WCallCommand?
@Published var showCallView = false
// remote desktop
@Published var remoteCtrlSession: RemoteCtrlSession?
// currently showing invitation
@Published var showingInvitation: ShowingInvitation?
// currently showing QR code
@Published var connReqInv: String?
// 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<Int64, () -> Void> = [:]
@@ -107,14 +102,12 @@ final class ChatModel: ObservableObject {
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
let ntfEnableLocal = true
var ntfEnablePeriodic: Bool {
notificationMode != .off
var ntfEnableLocal: Bool {
notificationMode == .off || ntfEnableLocalGroupDefault.get()
}
var activeRemoteCtrl: Bool {
remoteCtrlSession?.active ?? false
var ntfEnablePeriodic: Bool {
notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
}
func getUser(_ userId: Int64) -> User? {
@@ -268,20 +261,7 @@ final class ChatModel: ObservableObject {
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update previews
if let i = getChatIndex(cInfo.id) {
chats[i].chatItems = switch cInfo {
case .group:
if let currentPreviewItem = chats[i].chatItems.first {
if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
[cItem]
} else {
[currentPreviewItem]
}
} else {
[cItem]
}
default:
[cItem]
}
chats[i].chatItems = [cItem]
if case .rcvNew = cItem.meta.itemStatus {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
increaseUnreadCounter(user: currentUser!)
@@ -621,16 +601,14 @@ final class ChatModel: ObservableObject {
}
func dismissConnReqView(_ id: String) {
if id == showingInvitation?.connId {
markShowingInvitationUsed()
if let connReqInv = connReqInv,
let c = getChat(id),
case let .contactConnection(contactConnection) = c.chatInfo,
connReqInv == contactConnection.connReqInv {
dismissAllSheets()
}
}
func markShowingInvitationUsed() {
showingInvitation?.connChatUsed = true
}
func removeChat(_ id: String) {
withAnimation {
chats.removeAll(where: { $0.id == id })
@@ -707,11 +685,6 @@ final class ChatModel: ObservableObject {
}
}
struct ShowingInvitation {
var connId: String
var connChatUsed: Bool
}
struct NTFContactRequest {
var incognito: Bool
var chatId: String
@@ -789,38 +762,3 @@ final class GMember: ObservableObject, Identifiable {
var viewId: String { get { "\(wrapped.id) \(created.timeIntervalSince1970)" } }
static let sampleData = GMember(GroupMember.sampleData)
}
struct RemoteCtrlSession {
var ctrlAppInfo: CtrlAppInfo?
var appVersion: String
var sessionState: UIRemoteCtrlSessionState
func updateState(_ state: UIRemoteCtrlSessionState) -> RemoteCtrlSession {
RemoteCtrlSession(ctrlAppInfo: ctrlAppInfo, appVersion: appVersion, sessionState: state)
}
var active: Bool {
if case .connected = sessionState { true } else { false }
}
var discovery: Bool {
if case .searching = sessionState { true } else { false }
}
var sessionCode: String? {
switch sessionState {
case let .pendingConfirmation(_, sessionCode): sessionCode
case let .connected(_, sessionCode): sessionCode
default: nil
}
}
}
enum UIRemoteCtrlSessionState {
case starting
case searching
case found(remoteCtrl: RemoteCtrlInfo, compatible: Bool)
case connecting(remoteCtrl_: RemoteCtrlInfo?)
case pendingConfirmation(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
case connected(remoteCtrl: RemoteCtrlInfo, sessionCode: String)
}
+4 -4
View File
@@ -195,18 +195,18 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
}
}
func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
}
private func uniqueCombine(_ fileName: String, fullPath: Bool = false) -> String {
private func uniqueCombine(_ fileName: String) -> String {
func tryCombine(_ fileName: String, _ n: Int) -> String {
let ns = fileName as NSString
let name = ns.deletingPathExtension
let ext = ns.pathExtension
let suffix = (n == 0) ? "" : "_\(n)"
let f = "\(name)\(suffix).\(ext)"
return (FileManager.default.fileExists(atPath: fullPath ? f : getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
}
return tryCombine(fileName, 0)
}
-83
View File
@@ -1,83 +0,0 @@
//
// NSESubscriber.swift
// SimpleXChat
//
// Created by Evgeny on 09/12/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import Foundation
import SimpleXChat
private var nseSubscribers: [UUID:NSESubscriber] = [:]
// timeout for active notification service extension going into "suspending" state.
// If in two seconds the state does not change, we assume that it was not running and proceed with app activation/answering call.
private let SUSPENDING_TIMEOUT: TimeInterval = 2
// timeout should be larger than SUSPENDING_TIMEOUT
func waitNSESuspended(timeout: TimeInterval, dispatchQueue: DispatchQueue = DispatchQueue.main, suspended: @escaping (Bool) -> Void) {
if timeout <= SUSPENDING_TIMEOUT {
logger.warning("waitNSESuspended: small timeout \(timeout), using \(SUSPENDING_TIMEOUT + 1)")
}
var state = nseStateGroupDefault.get()
if case .suspended = state {
dispatchQueue.async { suspended(true) }
return
}
let id = UUID()
var suspendedCalled = false
checkTimeout()
nseSubscribers[id] = nseMessageSubscriber { msg in
if case let .state(newState) = msg {
state = newState
logger.debug("waitNSESuspended state: \(state.rawValue)")
if case .suspended = newState {
notifySuspended(true)
}
}
}
return
func notifySuspended(_ ok: Bool) {
logger.debug("waitNSESuspended notifySuspended: \(ok)")
if !suspendedCalled {
logger.debug("waitNSESuspended notifySuspended: calling suspended(\(ok))")
suspendedCalled = true
nseSubscribers.removeValue(forKey: id)
dispatchQueue.async { suspended(ok) }
}
}
func checkTimeout() {
if !suspending() {
checkSuspendingTimeout()
} else if state == .suspending {
checkSuspendedTimeout()
}
}
func suspending() -> Bool {
suspendedCalled || state == .suspended || state == .suspending
}
func checkSuspendingTimeout() {
DispatchQueue.global().asyncAfter(deadline: .now() + SUSPENDING_TIMEOUT) {
logger.debug("waitNSESuspended check suspending timeout")
if !suspending() {
notifySuspended(false)
} else if state != .suspended {
checkSuspendedTimeout()
}
}
}
func checkSuspendedTimeout() {
DispatchQueue.global().asyncAfter(deadline: .now() + min(timeout - SUSPENDING_TIMEOUT, 1)) {
logger.debug("waitNSESuspended check suspended timeout")
if state != .suspended {
notifySuspended(false)
}
}
}
}
+36 -130
View File
@@ -228,8 +228,7 @@ func apiStopChat() async throws {
}
func apiActivateChat() {
chatReopenStore()
let r = chatSendCmdSync(.apiActivateChat(restoreChat: true))
let r = chatSendCmdSync(.apiActivateChat)
if case .cmdOk = r { return }
logger.error("apiActivateChat error: \(String(describing: r))")
}
@@ -581,15 +580,15 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo
return nil
}
func apiAddContact(incognito: Bool) async -> ((String, PendingContactConnection)?, Alert?) {
func apiAddContact(incognito: Bool) async -> (String, PendingContactConnection)? {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiAddContact: no current user")
return (nil, nil)
return nil
}
let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false)
if case let .invitation(_, connReqInvitation, connection) = r { return ((connReqInvitation, connection), nil) }
let alert = connectionErrorAlert(r)
return (nil, alert)
if case let .invitation(_, connReqInvitation, connection) = r { return (connReqInvitation, connection) }
AlertManager.shared.showAlert(connectionErrorAlert(r))
return nil
}
func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? {
@@ -606,29 +605,27 @@ func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
throw r
}
func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, PendingContactConnection)? {
let (r, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
if let alert = alert {
AlertManager.shared.showAlert(alert)
return nil
} else {
return r
return connReqType
}
}
func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, PendingContactConnection)?, Alert?) {
func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnect: no current user")
return (nil, nil)
}
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq))
let m = ChatModel.shared
switch r {
case let .sentConfirmation(_, connection):
return ((.invitation, connection), nil)
case let .sentInvitation(_, connection):
return ((.contact, connection), nil)
case .sentConfirmation: return (.invitation, nil)
case .sentInvitation: return (.contact, nil)
case let .contactAlreadyExists(_, contact):
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id }
}
@@ -908,46 +905,6 @@ func apiCancelFile(fileId: Int64) async -> AChatItem? {
}
}
func setLocalDeviceName(_ displayName: String) throws {
try sendCommandOkRespSync(.setLocalDeviceName(displayName: displayName))
}
func connectRemoteCtrl(desktopAddress: String) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) {
let r = await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress))
if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) }
throw r
}
func findKnownRemoteCtrl() async throws {
try await sendCommandOkResp(.findKnownRemoteCtrl)
}
func confirmRemoteCtrl(_ rcId: Int64) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) {
let r = await chatSendCmd(.confirmRemoteCtrl(remoteCtrlId: rcId))
if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) }
throw r
}
func verifyRemoteCtrlSession(_ sessCode: String) async throws -> RemoteCtrlInfo {
let r = await chatSendCmd(.verifyRemoteCtrlSession(sessionCode: sessCode))
if case let .remoteCtrlConnected(rc) = r { return rc }
throw r
}
func listRemoteCtrls() throws -> [RemoteCtrlInfo] {
let r = chatSendCmdSync(.listRemoteCtrls)
if case let .remoteCtrlList(rcInfo) = r { return rcInfo }
throw r
}
func stopRemoteCtrl() async throws {
try await sendCommandOkResp(.stopRemoteCtrl)
}
func deleteRemoteCtrl(_ rcId: Int64) async throws {
try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId))
}
func networkErrorAlert(_ r: ChatResponse) -> Alert? {
switch r {
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
@@ -1076,12 +1033,6 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
throw r
}
private func sendCommandOkRespSync(_ cmd: ChatCommand) throws {
let r = chatSendCmdSync(cmd)
if case .cmdOk = r { return }
throw r
}
func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo {
let userId = try currentUserId("apiNewGroup")
let r = chatSendCmdSync(.apiNewGroup(userId: userId, incognito: incognito, groupProfile: groupProfile))
@@ -1235,9 +1186,6 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
try startChat(refreshInvitations: refreshInvitations)
} else {
m.chatRunning = false
try getUserChatData()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
m.onboardingStage = onboardingStageDefault.get()
}
}
@@ -1254,8 +1202,6 @@ func startChat(refreshInvitations: Bool = true) throws {
try refreshCallInvitations()
}
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
// when it is called before startChat
if let token = m.deviceToken {
registerToken(token: token)
}
@@ -1370,6 +1316,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
let m = ChatModel.shared
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
case let .newContactConnection(user, connection):
if active(user) {
await MainActor.run {
m.updateContactConnection(connection)
}
}
case let .contactConnectionDeleted(user, connection):
if active(user) {
await MainActor.run {
m.removeChat(connection.id)
}
}
case let .contactDeletedByContact(user, contact):
if active(user) && contact.directOrUsed {
await MainActor.run {
@@ -1662,40 +1620,36 @@ func processReceivedMsg(_ res: ChatResponse) async {
activateCall(invitation)
case let .callOffer(_, contact, callType, offer, sharedKey, _):
await withCall(contact) { call in
await MainActor.run {
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
}
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
let iceServers = getIceServers()
logger.debug(".callOffer useRelay \(useRelay)")
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
await m.callCommand.processCommand(.offer(
m.callCommand = .offer(
offer: offer.rtcSession,
iceCandidates: offer.rtcIceCandidates,
media: callType.media, aesKey: sharedKey,
iceServers: iceServers,
relay: useRelay
))
)
}
case let .callAnswer(_, contact, answer):
await withCall(contact) { call in
await MainActor.run {
call.callState = .answerReceived
}
await m.callCommand.processCommand(.answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates))
call.callState = .answerReceived
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
}
case let .callExtraInfo(_, contact, extraInfo):
await withCall(contact) { _ in
await m.callCommand.processCommand(.ice(iceCandidates: extraInfo.rtcIceCandidates))
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
}
case let .callEnded(_, contact):
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
CallController.shared.reportCallRemoteEnded(invitation: invitation)
}
await withCall(contact) { call in
await m.callCommand.processCommand(.end)
m.callCommand = .end
CallController.shared.reportCallRemoteEnded(call: call)
}
case .chatSuspended:
@@ -1716,67 +1670,19 @@ func processReceivedMsg(_ res: ChatResponse) async {
await MainActor.run {
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
}
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible):
await MainActor.run {
if let sess = m.remoteCtrlSession, case .searching = sess.sessionState {
let state = UIRemoteCtrlSessionState.found(remoteCtrl: remoteCtrl, compatible: compatible)
m.remoteCtrlSession = RemoteCtrlSession(
ctrlAppInfo: ctrlAppInfo_,
appVersion: appVersion,
sessionState: state
)
}
}
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode):
await MainActor.run {
let state = UIRemoteCtrlSessionState.pendingConfirmation(remoteCtrl_: remoteCtrl_, sessionCode: sessionCode)
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
}
case let .remoteCtrlConnected(remoteCtrl):
// TODO currently it is returned in response to command, so it is redundant
await MainActor.run {
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
}
case .remoteCtrlStopped:
// This delay is needed to cancel the session that fails on network failure,
// e.g. when user did not grant permission to access local network yet.
if let sess = m.remoteCtrlSession {
await MainActor.run {
m.remoteCtrlSession = nil
}
if case .connected = sess.sessionState {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
switchToLocalSession()
}
}
}
default:
logger.debug("unsupported event: \(res.responseType)")
}
func withCall(_ contact: Contact, _ perform: (Call) async -> Void) async {
func withCall(_ contact: Contact, _ perform: (Call) -> Void) async {
if let call = m.activeCall, call.contact.apiId == contact.apiId {
await perform(call)
await MainActor.run { perform(call) }
} else {
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
}
}
}
func switchToLocalSession() {
let m = ChatModel.shared
m.remoteCtrlSession = nil
do {
m.users = try listUsers()
try getUserChatData()
let statuses = (try apiGetNetworkStatuses()).map { s in (s.agentConnId, s.networkStatus) }
m.networkStatuses = Dictionary(uniqueKeysWithValues: statuses)
} catch let error {
logger.debug("error updating chat data: \(responseError(error))")
}
}
func active(_ user: any UserLike) -> Bool {
user.userId == ChatModel.shared.currentUser?.id
}
+23 -81
View File
@@ -9,28 +9,27 @@
import Foundation
import UIKit
import SimpleXChat
import SwiftUI
private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock")
let appSuspendTimeout: Int = 15 // seconds
let bgSuspendTimeout: Int = 5 // seconds
let terminationTimeout: Int = 3 // seconds
let activationDelay: TimeInterval = 1.5
private func _suspendChat(timeout: Int) {
// this is a redundant check to prevent logical errors, like the one fixed in this PR
let state = AppChatState.shared.value
let state = appStateGroupDefault.get()
if !state.canSuspend {
logger.error("_suspendChat called, current state: \(state.rawValue, privacy: .public)")
} else if ChatModel.ok {
AppChatState.shared.set(.suspending)
appStateGroupDefault.set(.suspending)
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
let endTask = beginBGTask(chatSuspended)
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
} else {
AppChatState.shared.set(.suspended)
appStateGroupDefault.set(.suspended)
}
}
@@ -42,16 +41,18 @@ func suspendChat() {
func suspendBgRefresh() {
suspendLockQueue.sync {
if case .bgRefresh = AppChatState.shared.value {
if case .bgRefresh = appStateGroupDefault.get() {
_suspendChat(timeout: bgSuspendTimeout)
}
}
}
private var terminating = false
func terminateChat() {
logger.debug("terminateChat")
suspendLockQueue.sync {
switch AppChatState.shared.value {
switch appStateGroupDefault.get() {
case .suspending:
// suspend instantly if already suspending
_chatSuspended()
@@ -63,6 +64,7 @@ func terminateChat() {
case .stopped:
chatCloseStore()
default:
terminating = true
// the store will be closed in _chatSuspended when event is received
_suspendChat(timeout: terminationTimeout)
}
@@ -71,7 +73,7 @@ func terminateChat() {
func chatSuspended() {
suspendLockQueue.sync {
if case .suspending = AppChatState.shared.value {
if case .suspending = appStateGroupDefault.get() {
_chatSuspended()
}
}
@@ -79,108 +81,48 @@ func chatSuspended() {
private func _chatSuspended() {
logger.debug("_chatSuspended")
AppChatState.shared.set(.suspended)
appStateGroupDefault.set(.suspended)
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.stop()
}
chatCloseStore()
}
func setAppState(_ appState: AppState) {
suspendLockQueue.sync {
AppChatState.shared.set(appState)
if terminating {
chatCloseStore()
}
}
func activateChat(appState: AppState = .active) {
logger.debug("DEBUGGING: activateChat")
terminating = false
suspendLockQueue.sync {
AppChatState.shared.set(appState)
appStateGroupDefault.set(appState)
if ChatModel.ok { apiActivateChat() }
logger.debug("DEBUGGING: activateChat: after apiActivateChat")
}
}
func initChatAndMigrate(refreshInvitations: Bool = true) {
terminating = false
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)
}
))
} else {
initialize(start: true)
}
}
func initialize(start: Bool) {
do {
try initializeChat(start: m.v3DBMigration.startChat && start, refreshInvitations: refreshInvitations)
m.v3DBMigration = v3DBMigrationDefault.get()
try initializeChat(start: m.v3DBMigration.startChat, refreshInvitations: refreshInvitations)
} catch let error {
AlertManager.shared.showAlertMsg(
title: start ? "Error starting chat" : "Error opening chat",
message: "Please contact developers.\nError: \(responseError(error))"
)
fatalError("Failed to start or load chats: \(responseError(error))")
}
}
}
func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) {
func startChatAndActivate() {
terminating = false
logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
}
if .active == AppChatState.shared.value {
completion()
} else if nseStateGroupDefault.get().inactive {
activate()
} else {
// setting app state to "activating" to notify NSE that it should suspend
setAppState(.activating)
waitNSESuspended(timeout: 10, dispatchQueue: dispatchQueue) { ok in
if !ok {
// if for some reason NSE failed to suspend,
// e.g., it crashed previously without setting its state to "suspended",
// set it to "suspended" state anyway, so that next time app
// does not have to wait when activating.
nseStateGroupDefault.set(.suspended)
}
if AppChatState.shared.value == .activating {
activate()
}
}
}
func activate() {
if .active != appStateGroupDefault.get() {
logger.debug("DEBUGGING: startChatAndActivate: before activateChat")
activateChat()
completion()
logger.debug("DEBUGGING: startChatAndActivate: after activateChat")
}
}
// appStateGroupDefault must not be used in the app directly, only via this singleton
class AppChatState {
static let shared = AppChatState()
private var value_ = appStateGroupDefault.get()
var value: AppState {
value_
}
func set(_ state: AppState) {
appStateGroupDefault.set(state)
sendAppState(state)
value_ = state
}
}
+41 -31
View File
@@ -16,15 +16,17 @@ struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatModel = ChatModel.shared
@ObservedObject var alertManager = AlertManager.shared
@Environment(\.scenePhase) var scenePhase
@State private var enteredBackgroundAuthenticated: TimeInterval? = nil
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var userAuthorized: Bool?
@State private var doAuthenticate = false
@State private var enteredBackground: TimeInterval? = nil
@State private var canConnectCall = false
@State private var lastSuccessfulUnlock: TimeInterval? = nil
@State private var showInitializationView = false
init() {
DispatchQueue.global(qos: .background).sync {
haskell_init()
// hs_init(0, nil)
}
hs_init(0, nil)
UserDefaults.standard.register(defaults: appDefaults)
setGroupDefaults()
registerGroupDefaults()
@@ -34,17 +36,22 @@ struct SimpleXApp: App {
}
var body: some Scene {
WindowGroup {
// contentAccessAuthenticationExtended has to be passed to ContentView on view initialization,
// so that it's computed by the time view renders, and not on event after rendering
ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
return WindowGroup {
ContentView(
doAuthenticate: $doAuthenticate,
userAuthorized: $userAuthorized,
canConnectCall: $canConnectCall,
lastSuccessfulUnlock: $lastSuccessfulUnlock,
showInitializationView: $showInitializationView
)
.environmentObject(chatModel)
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
}
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
showInitializationView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
initChatAndMigrate()
}
}
@@ -52,35 +59,30 @@ struct SimpleXApp: App {
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
// --- authentication
// see ContentView .onChange(of: scenePhase) for remaining authentication logic
if chatModel.contentViewAccessAuthenticated {
enteredBackgroundAuthenticated = ProcessInfo.processInfo.systemUptime
}
chatModel.contentViewAccessAuthenticated = false
// authentication ---
if CallController.useCallKit() && chatModel.activeCall != nil {
CallController.shared.shouldSuspendChat = true
} else {
suspendChat()
BGManager.shared.schedule()
}
if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
canConnectCall = false
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
case .active:
CallController.shared.shouldSuspendChat = false
let appState = AppChatState.shared.value
if appState != .stopped {
startChatAndActivate {
if appState.inactive && chatModel.chatRunning == true {
updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
updateCallInvitations()
}
}
let appState = appStateGroupDefault.get()
startChatAndActivate()
if appState.inactive && chatModel.chatRunning == true {
updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
updateCallInvitations()
}
}
doAuthenticate = authenticationExpired()
canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
default:
break
}
@@ -113,14 +115,22 @@ struct SimpleXApp: App {
}
private func authenticationExpired() -> Bool {
if let enteredBackgroundAuthenticated = enteredBackgroundAuthenticated {
if let enteredBackground = enteredBackground {
let delay = Double(UserDefaults.standard.integer(forKey: DEFAULT_LA_LOCK_DELAY))
return ProcessInfo.processInfo.systemUptime - enteredBackgroundAuthenticated >= delay
return ProcessInfo.processInfo.systemUptime - enteredBackground >= delay
} else {
return true
}
}
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
return false
}
}
private func updateChats() {
do {
let chats = try apiGetChats()
@@ -49,10 +49,10 @@ struct ActiveCallView: View {
}
.onDisappear {
logger.debug("ActiveCallView: disappear")
Task { await m.callCommand.setClient(nil) }
AppDelegate.keepScreenOn(false)
client?.endCall()
}
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
.background(.black)
.preferredColorScheme(.dark)
}
@@ -60,8 +60,19 @@ struct ActiveCallView: View {
private func createWebRTCClient() {
if client == nil && canConnectCall {
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
sendCommandToClient()
}
}
private func sendCommandToClient() {
if call == m.activeCall,
m.activeCall != nil,
let client = client,
let cmd = m.callCommand {
m.callCommand = nil
logger.debug("sendCallCommand: \(cmd.cmdType)")
Task {
await m.callCommand.setClient(client)
await client.sendCallCommand(command: cmd)
}
}
}
@@ -157,10 +168,8 @@ struct ActiveCallView: View {
}
case let .error(message):
logger.debug("ActiveCallView: command error: \(message)")
AlertManager.shared.showAlert(Alert(title: Text("Error"), message: Text(message)))
case let .invalid(type):
logger.debug("ActiveCallView: invalid response: \(type)")
AlertManager.shared.showAlert(Alert(title: Text("Invalid response"), message: Text(type)))
}
}
}
@@ -246,6 +255,7 @@ struct ActiveCallOverlay: View {
HStack {
Text(call.encryptionStatus)
if let connInfo = call.connectionInfo {
// Text("(") + Text(connInfo.text) + Text(", \(connInfo.protocolText))")
Text("(") + Text(connInfo.text) + Text(")")
}
}
+21 -22
View File
@@ -155,32 +155,31 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
if (!ChatModel.shared.chatInitialized) {
initChatAndMigrate(refreshInvitations: false)
}
startChatAndActivate(dispatchQueue: DispatchQueue.global()) {
self.shouldSuspendChat = true
// There are no invitations in the model, as it was processed by NSE
_ = try? justRefreshCallInvitations()
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
// Extract the call information from the push notification payload
let m = ChatModel.shared
if let contactId = payload.dictionaryPayload["contactId"] as? String,
let invitation = m.callInvitations[contactId] {
let update = self.cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID {
logger.debug("CallController: report pushkit call via CallKit")
let update = self.cxCallUpdate(invitation: invitation)
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
}
// Tell PushKit that the notification is handled.
completion()
startChatAndActivate()
shouldSuspendChat = true
// There are no invitations in the model, as it was processed by NSE
_ = try? justRefreshCallInvitations()
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
// Extract the call information from the push notification payload
let m = ChatModel.shared
if let contactId = payload.dictionaryPayload["contactId"] as? String,
let invitation = m.callInvitations[contactId] {
let update = cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID {
logger.debug("CallController: report pushkit call via CallKit")
let update = cxCallUpdate(invitation: invitation)
provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
}
} else {
self.reportExpiredCall(update: update, completion)
// Tell PushKit that the notification is handled.
completion()
}
} else {
self.reportExpiredCall(payload: payload, completion)
reportExpiredCall(update: update, completion)
}
} else {
reportExpiredCall(payload: payload, completion)
}
}
+8 -12
View File
@@ -22,7 +22,7 @@ class CallManager {
let m = ChatModel.shared
if let call = m.activeCall, call.callkitUUID == callUUID {
m.showCallView = true
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
m.callCommand = .capabilities(media: call.localMedia)
return true
}
return false
@@ -57,21 +57,19 @@ class CallManager {
m.activeCall = call
m.showCallView = true
Task {
await m.callCommand.processCommand(.start(
m.callCommand = .start(
media: invitation.callType.media,
aesKey: invitation.sharedKey,
iceServers: iceServers,
relay: useRelay
))
}
)
}
}
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
let m = ChatModel.shared
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
m.callCommand = .media(media: media, enable: enable)
return true
}
return false
@@ -96,13 +94,11 @@ class CallManager {
completed()
} else {
logger.debug("CallManager.endCall: ending call...")
m.callCommand = .end
m.activeCall = nil
m.showCallView = false
completed()
Task {
await m.callCommand.processCommand(.end)
await MainActor.run {
m.activeCall = nil
m.showCallView = false
completed()
}
do {
try await apiEndCall(call.contact)
} catch {
+14 -44
View File
@@ -335,50 +335,6 @@ extension WCallResponse: Encodable {
}
}
actor WebRTCCommandProcessor {
private var client: WebRTCClient? = nil
private var commands: [WCallCommand] = []
private var running: Bool = false
func setClient(_ client: WebRTCClient?) async {
logger.debug("WebRTC: setClient, commands count \(self.commands.count)")
self.client = client
if client != nil {
await processAllCommands()
} else {
commands.removeAll()
}
}
func processCommand(_ c: WCallCommand) async {
// logger.debug("WebRTC: process command \(c.cmdType)")
commands.append(c)
if !running && client != nil {
await processAllCommands()
}
}
func processAllCommands() async {
logger.debug("WebRTC: process all commands, commands count \(self.commands.count), client == nil \(self.client == nil)")
if let client = client {
running = true
while let c = commands.first, shouldRunCommand(client, c) {
commands.remove(at: 0)
await client.sendCallCommand(command: c)
logger.debug("WebRTC: processed cmd \(c.cmdType)")
}
running = false
}
}
func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool {
switch c {
case .capabilities, .start, .offer, .end: true
default: client.activeCall.wrappedValue != nil
}
}
}
struct ConnectionState: Codable, Equatable {
var connectionState: String
var iceConnectionState: String
@@ -402,12 +358,26 @@ struct ConnectionInfo: Codable, Equatable {
return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)"
}
}
var protocolText: String {
let unknown = NSLocalizedString("unknown", comment: "connection info")
let local = localCandidate?.protocol?.uppercased() ?? unknown
let localRelay = localCandidate?.relayProtocol?.uppercased() ?? unknown
let remote = remoteCandidate?.protocol?.uppercased() ?? unknown
let localText = localRelay == local || localCandidate?.relayProtocol == nil
? local
: "\(local) (\(localRelay))"
return local == remote
? localText
: "\(localText) / \(remote)"
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
struct RTCIceCandidate: Codable, Equatable {
var candidateType: RTCIceCandidateType?
var `protocol`: String?
var relayProtocol: String?
var sdpMid: String?
var sdpMLineIndex: Int?
var candidate: String
+78 -119
View File
@@ -18,11 +18,10 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}()
private static let ivTagBytes: Int = 28
private static let enableEncryption: Bool = true
private var chat_ctrl = getChatCtrl()
struct Call {
var connection: RTCPeerConnection
var iceCandidates: IceCandidates
var iceCandidates: [RTCIceCandidate]
var localMedia: CallMediaType
var localCamera: RTCVideoCapturer?
var localVideoSource: RTCVideoSource?
@@ -34,24 +33,10 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
var frameDecryptor: RTCFrameDecryptor?
}
actor IceCandidates {
private var candidates: [RTCIceCandidate] = []
func getAndClear() async -> [RTCIceCandidate] {
let cs = candidates
candidates = []
return cs
}
func append(_ c: RTCIceCandidate) async {
candidates.append(c)
}
}
private let rtcAudioSession = RTCAudioSession.sharedInstance()
private let audioQueue = DispatchQueue(label: "audio")
private var sendCallResponse: (WVAPIMessage) async -> Void
var activeCall: Binding<Call?>
private var activeCall: Binding<Call?>
private var localRendererAspectRatio: Binding<CGFloat?>
@available(*, unavailable)
@@ -75,7 +60,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
]
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ remoteIceCandidates: [RTCIceCandidate], _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
connection.delegate = self
createAudioSender(connection)
@@ -102,7 +87,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
return Call(
connection: connection,
iceCandidates: IceCandidates(),
iceCandidates: remoteIceCandidates,
localMedia: mediaType,
localCamera: localCamera,
localVideoSource: localVideoSource,
@@ -159,18 +144,26 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
logger.debug("starting incoming call - create webrtc session")
if activeCall.wrappedValue != nil { endCall() }
let encryption = WebRTCClient.enableEncryption
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay)
let call = initializeCall(iceServers?.toWebRTCIceServers(), [], media, encryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
let (offer, error) = await call.connection.offer()
if let offer = offer {
resp = .offer(
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())),
capabilities: CallCapabilities(encryption: encryption)
)
self.waitForMoreIceCandidates()
} else {
resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")")
call.connection.offer { answer in
Task {
let gotCandidates = await self.waitWithTimeout(10_000, stepMs: 1000, until: { self.activeCall.wrappedValue?.iceCandidates.count ?? 0 > 0 })
if gotCandidates {
await self.sendCallResponse(.init(
corrId: nil,
resp: .offer(
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(self.activeCall.wrappedValue?.iceCandidates ?? [])),
capabilities: CallCapabilities(encryption: encryption)
),
command: command)
)
} else {
self.endCall()
}
}
}
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
if activeCall.wrappedValue != nil {
@@ -179,21 +172,26 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
resp = .error(message: "accept: encryption is not supported")
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
let call = initializeCall(iceServers?.toWebRTCIceServers(), remoteIceCandidates, media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
let pc = call.connection
if let type = offer.type, let sdp = offer.sdp {
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
let (answer, error) = await pc.answer()
if let answer = answer {
pc.answer { answer in
self.addIceCandidates(pc, remoteIceCandidates)
resp = .answer(
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates()))
)
self.waitForMoreIceCandidates()
} else {
resp = .error(message: "answer error: \(error?.localizedDescription ?? "unknown error")")
// Task {
// try? await Task.sleep(nanoseconds: 32_000 * 1000000)
Task {
await self.sendCallResponse(.init(
corrId: nil,
resp: .answer(
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(call.iceCandidates))
),
command: command)
)
}
// }
}
} else {
resp = .error(message: "accept: remote description is not set")
@@ -236,7 +234,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
resp = .ok
}
case .end:
// TODO possibly, endCall should be called before returning .ok
await sendCallResponse(.init(corrId: nil, resp: .ok, command: command))
endCall()
}
@@ -245,33 +242,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
func getInitialIceCandidates() async -> [RTCIceCandidate] {
await untilIceComplete(timeoutMs: 750, stepMs: 150) {}
let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)")
return candidates
}
func waitForMoreIceCandidates() {
Task {
await untilIceComplete(timeoutMs: 12000, stepMs: 1500) {
let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
if candidates.count > 0 {
logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)")
await self.sendIceCandidates(candidates)
}
}
}
}
func sendIceCandidates(_ candidates: [RTCIceCandidate]) async {
await self.sendCallResponse(.init(
corrId: nil,
resp: .ice(iceCandidates: compressToBase64(input: encodeJSON(candidates))),
command: nil)
)
}
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
@@ -309,7 +279,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
logCrypto("encrypt", chat_encrypt_media(chat_ctrl, &key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes)
} else {
return nil
@@ -417,13 +387,12 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
audioSessionToDefaults()
}
func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async {
var t: UInt64 = 0
repeat {
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
t += stepMs
await action()
} while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete
func waitWithTimeout(_ timeoutMs: UInt64, stepMs: UInt64, until success: () -> Bool) async -> Bool {
let startedAt = DispatchTime.now()
while !success() && startedAt.uptimeNanoseconds + timeoutMs * 1000000 > DispatchTime.now().uptimeNanoseconds {
guard let _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) else { break }
}
return success()
}
}
@@ -436,33 +405,25 @@ extension WebRTC.RTCPeerConnection {
optionalConstraints: nil)
}
func offer() async -> (RTCSessionDescription?, Error?) {
await withCheckedContinuation { cont in
offer(for: mediaConstraints()) { (sdp, error) in
self.processSDP(cont, sdp, error)
func offer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
offer(for: mediaConstraints()) { (sdp, error) in
guard let sdp = sdp else {
return
}
}
}
func answer() async -> (RTCSessionDescription?, Error?) {
await withCheckedContinuation { cont in
answer(for: mediaConstraints()) { (sdp, error) in
self.processSDP(cont, sdp, error)
}
}
}
private func processSDP(_ cont: CheckedContinuation<(RTCSessionDescription?, Error?), Never>, _ sdp: RTCSessionDescription?, _ error: Error?) {
if let sdp = sdp {
self.setLocalDescription(sdp, completionHandler: { (error) in
if let error = error {
cont.resume(returning: (nil, error))
} else {
cont.resume(returning: (sdp, nil))
}
completion(sdp)
})
}
}
func answer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
answer(for: mediaConstraints()) { (sdp, error) in
guard let sdp = sdp else {
return
}
self.setLocalDescription(sdp, completionHandler: { (error) in
completion(sdp)
})
} else {
cont.resume(returning: (nil, error))
}
}
}
@@ -518,7 +479,6 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
default: enableSpeaker = false
}
setSpeakerEnabledAndConfigureSession(enableSpeaker)
case .connected: sendConnectedEvent(connection)
case .disconnected, .failed: endCall()
default: do {}
}
@@ -531,9 +491,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
Task {
await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil))
}
activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil, nil))
}
func peerConnection(_ connection: RTCPeerConnection, didRemove candidates: [WebRTC.RTCIceCandidate]) {
@@ -548,9 +506,10 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
lastReceivedMs lastDataReceivedMs: Int32,
changeReason reason: String) {
// logger.debug("Connection changed candidate \(reason) \(remote.debugDescription) \(remote.description)")
sendConnectedEvent(connection, local: local, remote: remote)
}
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection) {
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection, local: WebRTC.RTCIceCandidate, remote: WebRTC.RTCIceCandidate) {
connection.statistics { (stats: RTCStatisticsReport) in
stats.statistics.values.forEach { stat in
// logger.debug("Stat \(stat.debugDescription)")
@@ -558,25 +517,24 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
let localId = stat.values["localCandidateId"] as? String,
let remoteId = stat.values["remoteCandidateId"] as? String,
let localStats = stats.statistics[localId],
let remoteStats = stats.statistics[remoteId]
let remoteStats = stats.statistics[remoteId],
local.sdp.contains("\((localStats.values["ip"] as? String ?? "--")) \((localStats.values["port"] as? String ?? "--"))") &&
remote.sdp.contains("\((remoteStats.values["ip"] as? String ?? "--")) \((remoteStats.values["port"] as? String ?? "--"))")
{
Task {
await self.sendCallResponse(.init(
corrId: nil,
resp: .connected(connectionInfo: ConnectionInfo(
localCandidate: RTCIceCandidate(
candidateType: RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
protocol: localStats.values["protocol"] as? String,
sdpMid: nil,
sdpMLineIndex: nil,
candidate: ""
localCandidate: local.toCandidate(
RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
localStats.values["protocol"] as? String,
localStats.values["relayProtocol"] as? String
),
remoteCandidate: RTCIceCandidate(
candidateType: RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
protocol: remoteStats.values["protocol"] as? String,
sdpMid: nil,
sdpMLineIndex: nil,
candidate: ""))),
remoteCandidate: remote.toCandidate(
RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
remoteStats.values["protocol"] as? String,
remoteStats.values["relayProtocol"] as? String
))),
command: nil)
)
}
@@ -676,10 +634,11 @@ extension RTCIceCandidate {
}
extension WebRTC.RTCIceCandidate {
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?) -> RTCIceCandidate {
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?, _ relayProtocol: String?) -> RTCIceCandidate {
RTCIceCandidate(
candidateType: candidateType,
protocol: `protocol`,
relayProtocol: relayProtocol,
sdpMid: sdpMid,
sdpMLineIndex: Int(sdpMLineIndex),
candidate: sdp
@@ -165,8 +165,6 @@ struct CIRcvDecryptionError: View {
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .other:
message = Text("\(msgCount) messages failed to decrypt.") + Text("\n") + why
case .ratchetSync:
message = Text("Encryption re-negotiation failed.")
}
return message
}
@@ -28,9 +28,7 @@ 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?
@@ -254,12 +252,10 @@ struct FramedItemView: View {
}
private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View {
toggleSecrets(qi.formattedText, $showQuoteSecrets,
MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText, showSecrets: showQuoteSecrets)
.lineLimit(lines)
.font(.subheadline)
.padding(.bottom, 6)
)
MsgContentView(chat: chat, text: qi.text, formattedText: qi.formattedText)
.lineLimit(lines)
.font(.subheadline)
.padding(.bottom, 6)
}
private func ciQuoteIconView(_ image: String) -> some View {
@@ -282,15 +278,13 @@ 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 ft = text == "" ? [] : ci.formattedText
let v = toggleSecrets(ft, $showSecrets, MsgContentView(
let v = MsgContentView(
chat: chat,
text: text,
formattedText: ft,
formattedText: text == "" ? [] : ci.formattedText,
meta: ci.meta,
rightToLeft: rtl,
showSecrets: showSecrets
))
rightToLeft: rtl
)
.multilineTextAlignment(rtl ? .trailing : .leading)
.padding(.vertical, 6)
.padding(.horizontal, 12)
@@ -304,7 +298,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())
@@ -324,14 +318,6 @@ struct FramedItemView: View {
}
}
@ViewBuilder func toggleSecrets<V: View>(_ ft: [FormattedText]?, _ showSecrets: Binding<Bool>, _ 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
@@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
private let noTyping = Text(" ")
@@ -31,7 +31,6 @@ 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?
@@ -63,7 +62,7 @@ struct MsgContentView: View {
}
private func msgContentView() -> Text {
var v = messageText(text, formattedText, sender, showSecrets: showSecrets)
var v = messageText(text, formattedText, sender)
if let mt = meta {
if mt.isLive {
v = v + typingIndicator(mt.recent)
@@ -85,14 +84,14 @@ struct MsgContentView: View {
}
}
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false, showSecrets: Bool) -> Text {
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text {
let s = text
var res: Text
if let ft = formattedText, ft.count > 0 && ft.count <= 200 {
res = formatText(ft[0], preview, showSecret: showSecrets)
res = formatText(ft[0], preview)
var i = 1
while i < ft.count {
res = res + formatText(ft[i], preview, showSecret: showSecrets)
res = res + formatText(ft[i], preview)
i = i + 1
}
} else {
@@ -111,7 +110,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
}
}
private func formatText(_ ft: FormattedText, _ preview: Bool, showSecret: Bool) -> Text {
private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text {
let t = ft.text
if let f = ft.format {
switch (f) {
@@ -119,13 +118,7 @@ private func formatText(_ ft: FormattedText, _ preview: Bool, showSecret: Bool)
case .italic: return Text(t).italic()
case .strikeThrough: return Text(t).strikethrough()
case .snippet: return Text(t).font(.body.monospaced())
case .secret: return
showSecret
? Text(t)
: Text(AttributedString(t, attributes: AttributeContainer([
.foregroundColor: UIColor.clear as Any,
.backgroundColor: UIColor.secondarySystemFill as Any
])))
case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary)
case let .colored(color): return Text(t).foregroundColor(color.uiColor)
case .uri: return linkText(t, t, preview, prefix: "")
case let .simplexLink(linkType, simplexUri, smpHosts):
@@ -151,7 +144,7 @@ private func linkText(_ s: String, _ link: String, _ preview: Bool, prefix: Stri
]))).underline()
}
func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
private func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String {
linkType.description + " " + "(via \(smpHosts.first ?? "?"))"
}
@@ -163,8 +156,7 @@ struct MsgContentView_Previews: PreviewProvider {
text: chatItem.text,
formattedText: chatItem.formattedText,
sender: chatItem.memberDisplayName,
meta: chatItem.meta,
showSecrets: false
meta: chatItem.meta
)
.environmentObject(Chat.sampleData)
}
@@ -168,6 +168,7 @@ 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))
@@ -197,7 +198,7 @@ struct ChatItemInfoView: View {
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil) -> some View {
if text != "" {
TextBubble(text: text, formattedText: formattedText, sender: sender)
messageText(text, formattedText, sender)
} else {
Text("no text")
.italic()
@@ -205,17 +206,6 @@ 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
@@ -237,6 +227,7 @@ 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))
+7 -12
View File
@@ -250,8 +250,8 @@ struct ChatView: View {
}
private func searchToolbar() -> some View {
HStack(spacing: 12) {
HStack(spacing: 4) {
HStack {
HStack {
Image(systemName: "magnifyingglass")
TextField("Search", text: $searchText)
.focused($searchFocussed)
@@ -264,9 +264,9 @@ struct ChatView: View {
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary)
.background(Color(.tertiarySystemFill))
.background(Color(.secondarySystemBackground))
.cornerRadius(10.0)
Button ("Cancel") {
@@ -723,14 +723,9 @@ struct ChatView: View {
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
menu.append(replyUIAction(ci))
}
let fileSource = getLoadedFileSource(ci.file)
let fileExists = if let fs = fileSource, FileManager.default.fileExists(atPath: getAppFilePath(fs.filePath).path) { true } else { false }
let copyAndShareAllowed = !ci.content.text.isEmpty || (ci.content.msgContent?.isImage == true && fileExists)
if copyAndShareAllowed {
menu.append(shareUIAction(ci))
menu.append(copyUIAction(ci))
}
if let fileSource = fileSource, fileExists {
menu.append(shareUIAction(ci))
menu.append(copyUIAction(ci))
if let fileSource = getLoadedFileSource(ci.file) {
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
if image.imageData != nil {
menu.append(saveFileAction(fileSource))
@@ -104,7 +104,7 @@ struct ComposeState {
var sendEnabled: Bool {
switch preview {
case let .mediaPreviews(media): return !media.isEmpty
case .mediaPreviews: return true
case .voicePreview: return voiceMessageRecordingState == .finished
case .filePreview: return true
default: return !message.isEmpty || liveMessage != nil
@@ -384,10 +384,10 @@ struct ComposeView: View {
}
}
.sheet(isPresented: $showMediaPicker) {
LibraryMediaListPicker(addMedia: addMediaContent, selectionLimit: 10, finishedPreprocessing: finishedPreprocessingMediaContent) { itemsSelected in
await MainActor.run {
showMediaPicker = false
if itemsSelected {
LibraryMediaListPicker(media: $chosenMedia, selectionLimit: 10) { itemsSelected in
showMediaPicker = false
if itemsSelected {
DispatchQueue.main.async {
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: []))
}
}
@@ -488,30 +488,6 @@ struct ComposeView: View {
}
}
private func addMediaContent(_ content: UploadContent) async {
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
var newMedia: [(String, UploadContent?)] = []
if case var .mediaPreviews(media) = composeState.preview {
media.append((img, content))
newMedia = media
} else {
newMedia = [(img, content)]
}
await MainActor.run {
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: newMedia))
}
}
}
// When error occurs while converting video, remove media preview
private func finishedPreprocessingMediaContent() {
if case let .mediaPreviews(media) = composeState.preview, media.isEmpty {
DispatchQueue.main.async {
composeState = composeState.copy(preview: .noPreview)
}
}
}
private var maxFileSize: Int64 {
getMaxFileSize(.xftp)
}
@@ -51,8 +51,7 @@ struct ContextItemView: View {
MsgContentView(
chat: chat,
text: contextItem.text,
formattedText: contextItem.formattedText,
showSecrets: false
formattedText: contextItem.formattedText
)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)
@@ -116,6 +116,7 @@ struct ContactPreferencesView: View {
private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View {
Text(feature.enabledDescription(enabled))
.frame(height: 36, alignment: .topLeading)
}
private func savePreferences() {
@@ -157,7 +157,7 @@ struct AddGroupMembersViewCommon: View {
private func rolePicker() -> some View {
Picker("New member role", selection: $selectedRole) {
ForEach(GroupMemberRole.allCases) { role in
if role <= groupInfo.membership.memberRole && role != .author {
if role <= groupInfo.membership.memberRole {
Text(role.text)
}
}
@@ -16,6 +16,7 @@ struct GroupChatInfoView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@ObservedObject var chat: Chat
@Binding var groupInfo: GroupInfo
@ObservedObject private var alertManager = AlertManager.shared
@State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@@ -188,19 +188,17 @@ struct GroupMemberInfoView: View {
// this condition prevents re-setting picker
if !justOpened { return }
}
justOpened = false
DispatchQueue.main.async {
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
justOpened = false
}
.onChange(of: newRole) { newRole in
if newRole != member.memberRole {
@@ -28,7 +28,6 @@ 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 {
@@ -97,6 +96,7 @@ struct GroupPreferencesView: View {
}
} footer: {
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
.frame(height: 36, alignment: .topLeading)
}
}
@@ -103,10 +103,8 @@ struct GroupProfileView: View {
}
}
.sheet(isPresented: $showImagePicker) {
LibraryImagePicker(image: $chosenImage) { _ in
await MainActor.run {
showImagePicker = false
}
LibraryImagePicker(image: $chosenImage) {
didSelectItem in showImagePicker = false
}
}
.onChange(of: chosenImage) { image in
@@ -53,7 +53,8 @@ struct GroupWelcomeView: View {
}
private func textPreview() -> some View {
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil)
.allowsHitTesting(false)
.frame(minHeight: 140, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -17,7 +17,7 @@ struct ScanCodeView: View {
var body: some View {
VStack(alignment: .leading) {
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode)
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
Text("Scan security code from your contact's app.")
@@ -11,7 +11,7 @@ import SwiftUI
struct ChatHelp: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
@State private var newChatMenuOption: NewChatMenuOption? = nil
@State private var showAddChat = false
var body: some View {
ScrollView { chatHelp() }
@@ -39,12 +39,13 @@ struct ChatHelp: View {
HStack(spacing: 8) {
Text("Tap button ")
NewChatMenuButton(newChatMenuOption: $newChatMenuOption)
NewChatButton(showAddChat: $showAddChat)
Text("above, then choose:")
}
Text("**Add contact**: to create a new invitation link, or connect via a link you received.")
Text("**Create group**: to create a new group.")
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.")
}
.padding(.top, 24)
+23 -168
View File
@@ -12,14 +12,9 @@ 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 searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
@State private var newChatMenuOption: NewChatMenuOption? = nil
@State private var showAddChat = false
@State private var userPickerVisible = false
@State private var showConnectDesktop = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
@@ -53,20 +48,17 @@ struct ChatListView: View {
}
}
}
UserPicker(
showSettings: $showSettings,
showConnectDesktop: $showConnectDesktop,
userPickerVisible: $userPickerVisible
)
}
.sheet(isPresented: $showConnectDesktop) {
ConnectDesktopView()
UserPicker(showSettings: $showSettings, userPickerVisible: $userPickerVisible)
}
}
private var chatListView: some View {
VStack {
chatList
if chatModel.chats.count > 0 {
chatList.searchable(text: $searchText)
} else {
chatList
}
}
.onDisappear() { withAnimation { userPickerVisible = false } }
.refreshable {
@@ -85,9 +77,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 +116,7 @@ struct ChatListView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
switch chatModel.chatRunning {
case .some(true): NewChatMenuButton(newChatMenuOption: $newChatMenuOption)
case .some(true): NewChatButton(showAddChat: $showAddChat)
case .some(false): chatStoppedIcon()
case .none: EmptyView()
}
@@ -144,25 +136,11 @@ struct ChatListView: View {
@ViewBuilder private var chatList: some View {
let cs = filteredChats()
ZStack {
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)
}
.offset(x: -8)
List {
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
.padding(.trailing, -16)
.disabled(chatModel.chatRunning != true)
}
}
.onChange(of: chatModel.chatId) { _ in
@@ -196,7 +174,7 @@ struct ChatListView: View {
.padding(.trailing, 12)
connectButton("Tap to start a new chat") {
newChatMenuOption = .newContact
showAddChat = true
}
Spacer()
@@ -228,25 +206,22 @@ struct ChatListView: View {
}
private func filteredChats() -> [Chat] {
if let linkChatId = searchChatFilteredBySimplexLink {
return chatModel.chats.filter { $0.id == linkChatId }
} else {
let s = searchString()
return s == "" && !showUnreadAndFavorites
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
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 .contactRequest:
return s == "" || viewNameContains(cInfo, s)
case let .contactConnection(conn):
@@ -255,11 +230,6 @@ 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
@@ -271,121 +241,6 @@ 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<EnvironmentValues, RefreshAction?>, 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(
@@ -150,7 +150,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, showSecrets: false)
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true)
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
@@ -169,7 +169,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, showSecrets: false)
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true)
func attachment() -> String? {
switch cItem.content.msgContent {
@@ -164,28 +164,6 @@ 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())
@@ -13,7 +13,6 @@ struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool
@State var scrollViewContentSize: CGSize = .zero
@State var disableScrolling: Bool = true
@@ -63,13 +62,6 @@ struct UserPicker: View {
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
.frame(maxHeight: scrollViewContentSize.height)
menuButton("Use from desktop", icon: "desktopcomputer") {
showConnectDesktop = true
withAnimation {
userPickerVisible.toggle()
}
}
Divider()
menuButton("Settings", icon: "gearshape") {
showSettings = true
withAnimation {
@@ -93,7 +85,7 @@ struct UserPicker: View {
do {
m.users = try listUsers()
} catch let error {
logger.error("Error loading users \(responseError(error))")
logger.error("Error updating users \(responseError(error))")
}
}
}
@@ -152,8 +144,7 @@ struct UserPicker: View {
.overlay(DetermineWidth())
Spacer()
Image(systemName: icon)
.symbolRenderingMode(.monochrome)
.foregroundColor(.secondary)
// .frame(width: 24, alignment: .center)
}
.padding(.horizontal)
.padding(.vertical, 22)
@@ -179,7 +170,6 @@ struct UserPicker_Previews: PreviewProvider {
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker(
showSettings: Binding.constant(false),
showConnectDesktop: Binding.constant(false),
userPickerVisible: Binding.constant(true)
)
.environmentObject(m)
@@ -415,7 +415,7 @@ struct DatabaseView: View {
do {
try initializeChat(start: true)
m.chatDbChanged = false
AppChatState.shared.set(.active)
appStateGroupDefault.set(.active)
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
@@ -427,7 +427,7 @@ struct DatabaseView: View {
m.chatRunning = true
ChatReceiver.shared.start()
chatLastStartGroupDefault.set(Date.now)
AppChatState.shared.set(.active)
appStateGroupDefault.set(.active)
} catch let error {
runChat = false
alert = .error(title: "Error starting chat", error: responseError(error))
@@ -477,7 +477,7 @@ func stopChatAsync() async throws {
try await apiStopChat()
ChatReceiver.shared.stop()
await MainActor.run { ChatModel.shared.chatRunning = false }
AppChatState.shared.set(.stopped)
appStateGroupDefault.set(.stopped)
}
func deleteChatAsync() async throws {
+79 -97
View File
@@ -13,130 +13,112 @@ import SimpleXChat
struct LibraryImagePicker: View {
@Binding var image: UIImage?
var didFinishPicking: (_ didSelectImage: Bool) async -> Void
@State var mediaAdded = false
var didFinishPicking: (_ didSelectItems: Bool) -> Void
@State var images: [UploadContent] = []
var body: some View {
LibraryMediaListPicker(addMedia: addMedia, selectionLimit: 1, didFinishPicking: didFinishPicking)
}
private func addMedia(_ content: UploadContent) async {
if mediaAdded { return }
await MainActor.run {
mediaAdded = true
image = content.uiImage
}
LibraryMediaListPicker(media: $images, selectionLimit: 1, didFinishPicking: didFinishPicking)
.onChange(of: images) { _ in
if let img = images.first {
image = img.uiImage
}
}
}
}
struct LibraryMediaListPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
var addMedia: (_ content: UploadContent) async -> Void
@Binding var media: [UploadContent]
var selectionLimit: Int
var finishedPreprocessing: () -> Void = {}
var didFinishPicking: (_ didSelectItems: Bool) async -> Void
var didFinishPicking: (_ didSelectItems: Bool) -> Void
class Coordinator: PHPickerViewControllerDelegate {
let parent: LibraryMediaListPicker
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryMediaListPicker")
var media: [UploadContent] = []
var mediaCount: Int = 0
init(_ parent: LibraryMediaListPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
Task {
await parent.didFinishPicking(!results.isEmpty)
if results.isEmpty { return }
for r in results {
await loadItem(r.itemProvider)
}
parent.finishedPreprocessing()
parent.didFinishPicking(!results.isEmpty)
guard !results.isEmpty else {
return
}
}
private func loadItem(_ p: NSItemProvider) async {
logger.debug("LibraryMediaListPicker result")
if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
if let video = await loadVideo(p) {
await self.parent.addMedia(video)
logger.debug("LibraryMediaListPicker: added video")
}
} else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
if let img = await loadImageData(p) {
await self.parent.addMedia(img)
logger.debug("LibraryMediaListPicker: added image")
}
} else if p.canLoadObject(ofClass: UIImage.self) {
if let img = await loadImage(p) {
await self.parent.addMedia(.simpleImage(image: img))
logger.debug("LibraryMediaListPicker: added image")
}
}
}
private func loadImageData(_ p: NSItemProvider) async -> UploadContent? {
await withCheckedContinuation { cont in
loadFileURL(p, type: UTType.data) { url in
if let url = url {
let img = UploadContent.loadFromURL(url: url)
cont.resume(returning: img)
} else {
cont.resume(returning: nil)
}
}
}
}
private func loadImage(_ p: NSItemProvider) async -> UIImage? {
await withCheckedContinuation { cont in
p.loadObject(ofClass: UIImage.self) { obj, err in
if let err = err {
logger.error("LibraryMediaListPicker result image error: \(err.localizedDescription)")
cont.resume(returning: nil)
} else {
cont.resume(returning: obj as? UIImage)
}
}
}
}
private func loadVideo(_ p: NSItemProvider) async -> UploadContent? {
await withCheckedContinuation { cont in
loadFileURL(p, type: UTType.movie) { url in
if let url = url {
let tempUrl = URL(fileURLWithPath: generateNewFileName(getTempFilesDirectory().path + "/" + "rawvideo", url.pathExtension, fullPath: true))
let convertedVideoUrl = URL(fileURLWithPath: generateNewFileName(getTempFilesDirectory().path + "/" + "video", "mp4", fullPath: true))
do {
// logger.debug("LibraryMediaListPicker copyItem \(url) to \(tempUrl)")
try FileManager.default.copyItem(at: url, to: tempUrl)
} catch let err {
logger.error("LibraryMediaListPicker copyItem error: \(err.localizedDescription)")
return cont.resume(returning: nil)
}
Task {
let success = await makeVideoQualityLower(tempUrl, outputUrl: convertedVideoUrl)
try? FileManager.default.removeItem(at: tempUrl)
if success {
_ = ChatModel.shared.filesToDelete.insert(convertedVideoUrl)
let video = UploadContent.loadVideoFromURL(url: convertedVideoUrl)
return cont.resume(returning: video)
parent.media = []
media = []
mediaCount = results.count
for result in results {
logger.log("LibraryMediaListPicker result")
let p = result.itemProvider
if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
p.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
if let url = url {
let tempUrl = URL(fileURLWithPath: getTempFilesDirectory().path + "/" + generateNewFileName("video", url.pathExtension))
if ((try? FileManager.default.copyItem(at: url, to: tempUrl)) != nil) {
ChatModel.shared.filesToDelete.insert(tempUrl)
self.loadVideo(url: tempUrl, error: error)
}
try? FileManager.default.removeItem(at: convertedVideoUrl)
cont.resume(returning: nil)
}
}
} else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
self.loadImage(object: url, error: error)
}
} else if p.canLoadObject(ofClass: UIImage.self) {
p.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
self.loadImage(object: image, error: error)
}
}
} else {
dispatchQueue.sync { self.mediaCount -= 1}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
self.dispatchQueue.sync {
if self.parent.media.count == 0 {
logger.log("LibraryMediaListPicker: added \(self.media.count) images out of \(results.count)")
self.parent.media = self.media
}
}
}
}
private func loadFileURL(_ p: NSItemProvider, type: UTType, completion: @escaping (URL?) -> Void) {
p.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, err in
if let err = err {
logger.error("LibraryMediaListPicker loadFileURL error: \(err.localizedDescription)")
completion(nil)
} else {
completion(url)
func loadImage(object: Any?, error: Error? = nil) {
if let error = error {
logger.error("LibraryMediaListPicker: couldn't load image with error: \(error.localizedDescription)")
} else if let image = object as? UIImage {
media.append(.simpleImage(image: image))
logger.log("LibraryMediaListPicker: added image")
} else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) {
media.append(image)
}
dispatchQueue.sync {
self.mediaCount -= 1
if self.mediaCount == 0 && self.parent.media.count == 0 {
logger.log("LibraryMediaListPicker: added all media")
self.parent.media = self.media
self.media = []
}
}
}
func loadVideo(url: URL?, error: Error? = nil) {
if let error = error {
logger.error("LibraryMediaListPicker: couldn't load video with error: \(error.localizedDescription)")
} else if let url = url as URL?, let video = UploadContent.loadVideoFromURL(url: url) {
media.append(video)
}
dispatchQueue.sync {
self.mediaCount -= 1
if self.mediaCount == 0 && self.parent.media.count == 0 {
logger.log("LibraryMediaListPicker: added all media")
self.parent.media = self.media
self.media = []
}
}
}
@@ -1,26 +0,0 @@
//
// VideoUtils.swift
// SimpleX (iOS)
//
// Created by Avently on 25.12.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import AVFoundation
import Foundation
import SimpleXChat
func makeVideoQualityLower(_ input: URL, outputUrl: URL) async -> Bool {
let asset: AVURLAsset = AVURLAsset(url: input, options: nil)
if let s = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) {
s.outputURL = outputUrl
s.outputFileType = .mp4
s.metadataItemFilter = AVMetadataItemFilter.forSharing()
await s.export()
if let err = s.error {
logger.error("Failed to export video with error: \(err)")
}
return s.status == .completed
}
return false
}
@@ -52,7 +52,7 @@ struct LocalAuthView: View {
resetChatCtrl()
try initializeChat(start: true)
m.chatDbChanged = false
AppChatState.shared.set(.active)
appStateGroupDefault.set(.active)
if m.currentUser != nil { return }
var profile: Profile? = nil
if let displayName = displayName, displayName != "" {
@@ -9,20 +9,8 @@
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.")
@@ -35,6 +23,6 @@ struct AddContactLearnMore: View {
struct AddContactLearnMore_Previews: PreviewProvider {
static var previews: some View {
AddContactLearnMore(showTitle: true)
AddContactLearnMore()
}
}
@@ -0,0 +1,129 @@
//
// 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"
)
}
}
@@ -130,10 +130,8 @@ struct AddGroupView: View {
}
}
.sheet(isPresented: $showImagePicker) {
LibraryImagePicker(image: $chosenImage) { _ in
await MainActor.run {
showImagePicker = false
}
LibraryImagePicker(image: $chosenImage) {
didSelectItem in showImagePicker = false
}
}
.alert(isPresented: $showInvalidNameAlert) {
@@ -187,7 +185,6 @@ 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)
@@ -0,0 +1,42 @@
//
// 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()
}
}
@@ -0,0 +1,93 @@
//
// 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 {
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)
}
}
@@ -0,0 +1,460 @@
//
// 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()) {
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 = await apiConnect(incognito: incognito, connReq: connectionLink) {
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))
}
}
@@ -1,52 +0,0 @@
//
// 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)
)
}
@@ -1,959 +0,0 @@
//
// 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<ScanResult, ScanError>) {
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<Content: View>: 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
)
}
@@ -0,0 +1,106 @@
//
// 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()
}
}
+21 -17
View File
@@ -11,12 +11,20 @@ import CoreImage.CIFilterBuiltins
struct MutableQRCode: View {
@Binding var uri: String
var withLogo: Bool = true
var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1)
@State private var image: UIImage?
var body: some View {
QRCode(uri: uri, withLogo: withLogo, tintColor: tintColor)
.id("simplex-qrcode-view-for-\(uri)")
ZStack {
if let image = image {
qrCodeImage(image)
}
}
.onAppear {
image = generateImage(uri)
}
.onChange(of: uri) { _ in
image = generateImage(uri)
}
}
}
@@ -24,10 +32,9 @@ 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, onShare: onShare)
QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor)
}
}
@@ -41,9 +48,8 @@ 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 = {}
@State private var makeScreenshotBinding: () -> Void = {}
var body: some View {
ZStack {
@@ -64,20 +70,18 @@ struct QRCode: View {
}
}
.onAppear {
makeScreenshotFunc = {
makeScreenshotBinding = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
onShare?()
}
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])}
}
.frame(width: geo.size.width, height: geo.size.height)
}
}
.onTapGesture(perform: makeScreenshotFunc)
.onTapGesture(perform: makeScreenshotBinding)
.onAppear {
image = image ?? generateImage(uri, tintColor: tintColor)
image = image ?? generateImage(uri)?.replaceColor(UIColor.black, tintColor)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -89,13 +93,13 @@ private func qrCodeImage(_ image: UIImage) -> some View {
.textSelection(.enabled)
}
private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? {
private func generateImage(_ uri: String) -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8)
if let outputImage = filter.outputImage,
let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor)
return UIImage(cgImage: cgImage)
}
return nil
}
@@ -0,0 +1,79 @@
//
// 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], 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<ScanResult, ScanError>) {
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()
}
}
@@ -81,6 +81,11 @@ struct CreateSimpleXAddress: View {
DispatchQueue.main.async {
m.userAddress = UserContactLink(connReqContact: connReqContact)
}
if let u = try await apiSetProfileAddress(on: true) {
DispatchQueue.main.async {
m.updateUser(u)
}
}
await MainActor.run { progressIndicator = false }
} catch let error {
logger.error("CreateSimpleXAddress create address: \(responseError(error))")
@@ -95,7 +100,7 @@ struct CreateSimpleXAddress: View {
} label: {
Text("Create SimpleX address").font(.title)
}
Text("You can make it visible to your SimpleX contacts via Settings.")
Text("Your contacts in SimpleX will see it.\nYou can change it in Settings.")
.multilineTextAlignment(.center)
.font(.footnote)
.padding(.horizontal, 32)
@@ -283,37 +283,6 @@ private let versionDescriptions: [VersionDescription] = [
),
]
),
VersionDescription(
version: "v5.4",
post: URL(string: "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html"),
features: [
FeatureDescription(
icon: "desktopcomputer",
title: "Link mobile and desktop apps! 🔗",
description: "Via secure quantum resistant protocol."
),
FeatureDescription(
icon: "person.2",
title: "Better groups",
description: "Faster joining and more reliable messages."
),
FeatureDescription(
icon: "theatermasks",
title: "Incognito groups",
description: "Create a group using a random profile."
),
FeatureDescription(
icon: "hand.raised",
title: "Block group members",
description: "To hide unwanted messages."
),
FeatureDescription(
icon: "gift",
title: "A few more things",
description: "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!"
),
]
),
]
private let lastVersion = versionDescriptions.last!.version
@@ -1,556 +0,0 @@
//
// ConnectDesktopView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 13/10/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
import CodeScanner
struct ConnectDesktopView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
var viaSettings = false
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) private var connectRemoteViaMulticastAuto = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var sessionAddress: String = ""
@State private var remoteCtrls: [RemoteCtrlInfo] = []
@State private var alert: ConnectDesktopAlert?
@State private var showConnectScreen = true
@State private var showQRCodeScanner = true
@State private var firstAppearance = true
private var useMulticast: Bool {
connectRemoteViaMulticast && !remoteCtrls.isEmpty
}
private enum ConnectDesktopAlert: Identifiable {
case unlinkDesktop(rc: RemoteCtrlInfo)
case disconnectDesktop(action: UserDisconnectAction)
case badInvitationError
case badVersionError(version: String?)
case desktopDisconnectedError
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
case let .unlinkDesktop(rc): "unlinkDesktop \(rc.remoteCtrlId)"
case let .disconnectDesktop(action): "disconnectDecktop \(action)"
case .badInvitationError: "badInvitationError"
case let .badVersionError(v): "badVersionError \(v ?? "")"
case .desktopDisconnectedError: "desktopDisconnectedError"
case let .error(title, _): "error \(title)"
}
}
}
private enum UserDisconnectAction: String {
case back
case dismiss // TODO dismiss settings after confirmation
}
var body: some View {
if viaSettings {
viewBody
.modifier(BackButton(label: "Back") {
if m.activeRemoteCtrl {
alert = .disconnectDesktop(action: .back)
} else {
dismiss()
}
})
} else {
NavigationView {
viewBody
}
}
}
var viewBody: some View {
Group {
let discovery = m.remoteCtrlSession?.discovery
if discovery == true || (discovery == nil && !showConnectScreen) {
searchingDesktopView()
} else if let session = m.remoteCtrlSession {
switch session.sessionState {
case .starting: connectingDesktopView(session, nil)
case .searching: searchingDesktopView()
case let .found(rc, compatible): foundDesktopView(session, rc, compatible)
case let .connecting(rc_): connectingDesktopView(session, rc_)
case let .pendingConfirmation(rc_, sessCode):
if confirmRemoteSessions || rc_ == nil {
verifySessionView(session, rc_, sessCode)
} else {
connectingDesktopView(session, rc_).onAppear {
verifyDesktopSessionCode(sessCode)
}
}
case let .connected(rc, _): activeSessionView(session, rc)
}
// The hack below prevents camera freezing when exiting linked devices view.
// Using showQRCodeScanner inside connectDesktopView or passing it as parameter still results in freezing.
} else if showQRCodeScanner || firstAppearance {
connectDesktopView()
} else {
connectDesktopView(showScanner: false)
}
}
.onAppear {
setDeviceName(deviceName)
updateRemoteCtrls()
showConnectScreen = !useMulticast
if m.remoteCtrlSession != nil {
disconnectDesktop()
} else if useMulticast {
findKnownDesktop()
}
// The hack below prevents camera freezing when exiting linked devices view.
// `firstAppearance` prevents camera flicker when the view first opens.
// moving `showQRCodeScanner = false` to `onDisappear` (to avoid `firstAppearance`) does not prevent freeze.
showQRCodeScanner = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
firstAppearance = false
showQRCodeScanner = true
}
}
.onDisappear {
if m.remoteCtrlSession != nil {
showConnectScreen = false
disconnectDesktop()
}
}
.onChange(of: deviceName) {
setDeviceName($0)
}
.onChange(of: m.activeRemoteCtrl) {
UIApplication.shared.isIdleTimerDisabled = $0
}
.alert(item: $alert) { a in
switch a {
case let .unlinkDesktop(rc):
Alert(
title: Text("Unlink desktop?"),
primaryButton: .destructive(Text("Unlink")) {
unlinkDesktop(rc)
},
secondaryButton: .cancel()
)
case let .disconnectDesktop(action):
Alert(
title: Text("Disconnect desktop?"),
primaryButton: .destructive(Text("Disconnect")) {
disconnectDesktop(action)
},
secondaryButton: .cancel()
)
case .badInvitationError:
Alert(title: Text("Bad desktop address"))
case let .badVersionError(v):
Alert(
title: Text("Incompatible version"),
message: Text("Desktop app version \(v ?? "") is not compatible with this app.")
)
case .desktopDisconnectedError:
Alert(title: Text("Connection terminated"))
case let .error(title, error):
Alert(title: Text(title), message: Text(error))
}
}
.interactiveDismissDisabled(m.activeRemoteCtrl)
}
private func connectDesktopView(showScanner: Bool = true) -> some View {
List {
Section("This device name") {
devicesView()
}
if showScanner {
scanDesctopAddressView()
}
if developerTools {
desktopAddressView()
}
}
.navigationTitle("Connect to desktop")
}
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
List {
Section("Connecting to desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
}
if let sessCode = session.sessionCode {
Section("Session code") {
sessionCodeText(sessCode)
}
}
Section {
disconnectButton()
}
}
.navigationTitle("Connecting to desktop")
}
private func searchingDesktopView() -> some View {
List {
Section("This device name") {
devicesView()
}
Section("Found desktop") {
Text("Waiting for desktop...").italic()
Button {
disconnectDesktop()
} label: {
Label("Scan QR code", systemImage: "qrcode")
}
}
}
.navigationTitle("Connecting to desktop")
}
@ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View {
let v = List {
Section("This device name") {
devicesView()
}
Section("Found desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
if !compatible {
Text("Not compatible!").foregroundColor(.red)
} else if !connectRemoteViaMulticastAuto {
Button {
confirmKnownDesktop(rc)
} label: {
Label("Connect", systemImage: "checkmark")
}
}
}
if !compatible && !connectRemoteViaMulticastAuto {
Section {
disconnectButton("Cancel")
}
}
}
.navigationTitle("Found desktop")
if compatible && connectRemoteViaMulticastAuto {
v.onAppear { confirmKnownDesktop(rc) }
} else {
v
}
}
private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View {
List {
Section("Connected to desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
}
Section("Verify code with desktop") {
sessionCodeText(sessCode)
Button {
verifyDesktopSessionCode(sessCode)
} label: {
Label("Confirm", systemImage: "checkmark")
}
}
Section {
disconnectButton()
}
}
.navigationTitle("Verify connection")
}
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "")
if (rc == nil) {
t = t + Text(" ") + Text("(new)").italic()
}
return t
}
private func ctrlDeviceVersionText(_ session: RemoteCtrlSession) -> Text {
let v = session.ctrlAppInfo?.appVersionRange.maxVersion
var t = Text("v\(v ?? "")")
if v != session.appVersion {
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
}
return t
}
private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View {
List {
Section("Connected desktop") {
Text(rc.deviceViewName)
ctrlDeviceVersionText(session)
}
if let sessCode = session.sessionCode {
Section("Session code") {
sessionCodeText(sessCode)
}
}
Section {
disconnectButton()
} footer: {
// This is specific to iOS
Text("Keep the app open to use it from desktop")
}
}
.navigationTitle("Connected to desktop")
}
private func sessionCodeText(_ code: String) -> some View {
Text(code.prefix(23))
}
private func devicesView() -> some View {
Group {
TextField("Enter this device name…", text: $deviceName)
if !remoteCtrls.isEmpty {
NavigationLink {
linkedDesktopsView()
} label: {
Text("Linked desktops")
}
}
}
}
private func scanDesctopAddressView() -> some View {
Section("Scan QR code from desktop") {
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.padding(.horizontal)
}
}
private func desktopAddressView() -> some View {
Section("Desktop address") {
if sessionAddress.isEmpty {
Button {
sessionAddress = UIPasteboard.general.string ?? ""
} label: {
Label("Paste desktop address", systemImage: "doc.plaintext")
}
.disabled(!UIPasteboard.general.hasStrings)
} else {
HStack {
Text(sessionAddress).lineLimit(1)
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
.onTapGesture { sessionAddress = "" }
}
}
Button {
connectDesktopAddress(sessionAddress)
} label: {
Label("Connect to desktop", systemImage: "rectangle.connected.to.line.below")
}
.disabled(sessionAddress.isEmpty)
}
}
private func linkedDesktopsView() -> some View {
List {
Section("Desktop devices") {
ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in
remoteCtrlView(rc)
}
.onDelete { indexSet in
if let i = indexSet.first, i < remoteCtrls.count {
alert = .unlinkDesktop(rc: remoteCtrls[i])
}
}
}
Section("Linked desktop options") {
Toggle("Verify connections", isOn: $confirmRemoteSessions)
Toggle("Discover via local network", isOn: $connectRemoteViaMulticast)
if connectRemoteViaMulticast {
Toggle("Connect automatically", isOn: $connectRemoteViaMulticastAuto)
}
}
}
.navigationTitle("Linked desktops")
}
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
Text(rc.deviceViewName)
}
private func setDeviceName(_ name: String) {
do {
try setLocalDeviceName(deviceName)
} catch let e {
errorAlert(e)
}
}
private func updateRemoteCtrls() {
do {
remoteCtrls = try listRemoteCtrls()
} catch let e {
errorAlert(e)
}
}
private func processDesktopQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r): connectDesktopAddress(r.string)
case let .failure(e): errorAlert(e)
}
}
private func findKnownDesktop() {
Task {
do {
try await findKnownRemoteCtrl()
await MainActor.run {
m.remoteCtrlSession = RemoteCtrlSession(
ctrlAppInfo: nil,
appVersion: "",
sessionState: .searching
)
showConnectScreen = true
}
} catch let e {
await MainActor.run {
errorAlert(e)
}
}
}
}
private func confirmKnownDesktop(_ rc: RemoteCtrlInfo) {
connectDesktop_ {
try await confirmRemoteCtrl(rc.remoteCtrlId)
}
}
private func connectDesktopAddress(_ addr: String) {
connectDesktop_ {
try await connectRemoteCtrl(desktopAddress: addr)
}
}
private func connectDesktop_(_ connect: @escaping () async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String)) {
Task {
do {
let (rc_, ctrlAppInfo, v) = try await connect()
await MainActor.run {
sessionAddress = ""
m.remoteCtrlSession = RemoteCtrlSession(
ctrlAppInfo: ctrlAppInfo,
appVersion: v,
sessionState: .connecting(remoteCtrl_: rc_)
)
}
} catch let e {
await MainActor.run {
switch e as? ChatResponse {
case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError
case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError
case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v)
case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil)
case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError
default: errorAlert(e)
}
}
}
}
}
private func verifyDesktopSessionCode(_ sessCode: String) {
Task {
do {
let rc = try await verifyRemoteCtrlSession(sessCode)
await MainActor.run {
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(.connected(remoteCtrl: rc, sessionCode: sessCode))
}
await MainActor.run {
updateRemoteCtrls()
}
} catch let error {
await MainActor.run {
errorAlert(error)
}
}
}
}
private func disconnectButton(_ label: LocalizedStringKey = "Disconnect") -> some View {
Button {
disconnectDesktop(.dismiss)
} label: {
Label(label, systemImage: "multiply")
}
}
private func disconnectDesktop(_ action: UserDisconnectAction? = nil) {
Task {
do {
try await stopRemoteCtrl()
await MainActor.run {
if case .connected = m.remoteCtrlSession?.sessionState {
switchToLocalSession()
} else {
m.remoteCtrlSession = nil
}
switch action {
case .back: dismiss()
case .dismiss: dismiss()
case .none: ()
}
}
} catch let e {
await MainActor.run {
errorAlert(e)
}
}
}
}
private func unlinkDesktop(_ rc: RemoteCtrlInfo) {
Task {
do {
try await deleteRemoteCtrl(rc.remoteCtrlId)
await MainActor.run {
remoteCtrls.removeAll(where: { $0.remoteCtrlId == rc.remoteCtrlId })
}
} catch let e {
await MainActor.run {
errorAlert(e)
}
}
}
}
private func errorAlert(_ error: Error) {
let a = getErrorAlert(error, "Error")
alert = .error(title: a.title, error: a.message)
}
}
#Preview {
ConnectDesktopView()
}
@@ -51,9 +51,9 @@ struct AdvancedNetworkSettings: View {
}
.disabled(currentNetCfg == NetCfg.proxyDefaults)
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 45_000, 60_000, 90_000, 120_000], label: secondsLabel)
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [3_000000, 5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 60_000, 90_000, 120_000], label: secondsLabel)
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
@@ -10,23 +10,24 @@ import SwiftUI
struct IncognitoHelp: View {
var body: some View {
List {
VStack(alignment: .leading) {
Text("Incognito mode")
.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("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).")
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)
}
.frame(maxWidth: .infinity)
.padding()
}
}
@@ -14,6 +14,9 @@ struct NotificationsView: View {
@State private var notificationMode: NotificationsMode = ChatModel.shared.notificationMode
@State private var showAlert: NotificationAlert?
@State private var legacyDatabase = dbContainerGroupDefault.get() == .documents
// @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
// @AppStorage(GROUP_DEFAULT_NTF_ENABLE_LOCAL, store: groupDefaults) private var ntfEnableLocal = false
// @AppStorage(GROUP_DEFAULT_NTF_ENABLE_PERIODIC, store: groupDefaults) private var ntfEnablePeriodic = false
var body: some View {
List {
@@ -85,6 +88,13 @@ struct NotificationsView: View {
.padding(.top, 1)
}
}
// if developerTools {
// Section(String("Experimental")) {
// Toggle(String("Always enable local"), isOn: $ntfEnableLocal)
// Toggle(String("Always enable periodic"), isOn: $ntfEnablePeriodic)
// }
// }
}
.disabled(legacyDatabase)
}
@@ -109,7 +119,7 @@ struct NotificationsView: View {
private func ntfModeAlertTitle(_ mode: NotificationsMode) -> LocalizedStringKey {
switch mode {
case .off: return "Use only local notifications?"
case .off: return "Turn off notifications?"
case .periodic: return "Enable periodic notifications?"
case .instant: return "Enable instant notifications?"
}
@@ -63,6 +63,7 @@ struct PreferencesView: View {
private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View {
Text(feature.allowDescription(allowFeature.wrappedValue))
.frame(height: 36, alignment: .topLeading)
}
private func savePreferences() {
@@ -467,7 +467,6 @@ struct SimplexLockView: View {
switch a {
case .enableAuth:
SetAppPasscodeView {
m.contentViewAccessAuthenticated = true
laLockDelay = 30
prefPerformLA = true
showChangePassword = true
@@ -620,7 +619,6 @@ struct SimplexLockView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
m.contentViewAccessAuthenticated = true
prefPerformLA = true
laAlert = .laTurnedOnAlert
case .failed:
@@ -21,7 +21,7 @@ struct ScanProtocolServer: View {
.font(.largeTitle)
.bold()
.padding(.vertical)
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processQRCode)
CodeScannerView(codeTypes: [.qr], completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.padding(.top)
@@ -53,10 +53,6 @@ let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites"
let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
let appDefaults: [String: Any] = [
DEFAULT_SHOW_LA_NOTICE: false,
@@ -89,18 +85,9 @@ let appDefaults: [String: Any] = [
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue,
DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300,
DEFAULT_SHOW_UNREAD_AND_FAVORITES: false,
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
DEFAULT_SHOW_UNREAD_AND_FAVORITES: false
]
// not used anymore
enum ConnectViaLinkTab: String {
case scan
case paste
}
enum SimpleXLinkMode: String, Identifiable {
case description
case full
@@ -191,12 +178,6 @@ struct SettingsView: View {
} label: {
settingsRow("switch.2") { Text("Chat preferences") }
}
NavigationLink {
ConnectDesktopView(viaSettings: true)
} label: {
settingsRow("desktopcomputer") { Text("Use from desktop") }
}
}
.disabled(chatModel.chatRunning != true)
@@ -381,9 +362,7 @@ struct SettingsView: View {
func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View {
ZStack(alignment: .leading) {
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.symbolRenderingMode(.monochrome)
.foregroundColor(color)
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(color)
content().padding(.leading, indent)
}
}
@@ -190,8 +190,7 @@ struct UserAddressView: View {
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
Section {
SimpleXLinkQRCode(uri: userAddress.connReqContact)
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact)))
shareQRCodeButton(userAddress)
if MFMailComposeViewController.canSendMail() {
shareViaEmailButton(userAddress)
@@ -120,10 +120,8 @@ struct UserProfile: View {
}
}
.sheet(isPresented: $showImagePicker) {
LibraryImagePicker(image: $chosenImage) { _ in
await MainActor.run {
showImagePicker = false
}
LibraryImagePicker(image: $chosenImage) {
didSelectItem in showImagePicker = false
}
}
.onChange(of: chosenImage) { image in
-4
View File
@@ -18,9 +18,5 @@
<array>
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
</array>
<key>com.apple.developer.networking.multicast</key>
<true/>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
</dict>
</plist>
@@ -290,30 +290,19 @@
<target>(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
<source>)</source>
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**Създай линк / QR код**, който вашият контакт да използва.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -326,6 +315,11 @@
<target>**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**Поставете получения линк** или го отворете в браузъра и докоснете **Отваряне в мобилно приложение**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите.</target>
@@ -336,6 +330,11 @@
<target>**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**Сканирай QR код**: за да се свържете с вашия контакт лично или чрез видеообаждане.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**Внимание**: Незабавните push известия изискват парола, запазена в Keychain.</target>
@@ -379,12 +378,6 @@
- и още!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -433,6 +426,11 @@
<target>1 седмица</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>Еднократен линк</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 минути</target>
@@ -548,10 +546,6 @@
<target>Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>Добави предварително зададени сървъри</target>
@@ -864,10 +858,6 @@
<target>Назад</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source>
<target>Лошо ID на съобщението</target>
@@ -878,10 +868,6 @@
<target>Лош хеш на съобщението</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>По-добри съобщения</target>
@@ -891,10 +877,6 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
@@ -948,10 +930,6 @@
<target>Обаждания</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>Потребителският профил не може да се изтрие!</target>
@@ -1068,10 +1046,6 @@
<target>Чатът е спрян</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>Чат настройки</target>
@@ -1172,19 +1146,11 @@
<target>Свързване</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve">
<source>Connect automatically</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>Свързване инкогнито</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve">
<source>Connect to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve">
<source>Connect to yourself?</source>
<note>No comment provided by engineer.</note>
@@ -1208,6 +1174,11 @@ This is your own one-time link!</source>
<target>Свърване чрез линк</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>Свърване чрез линк/QR код</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>Свързване чрез еднократен линк за връзка</target>
@@ -1217,14 +1188,6 @@ This is your own one-time link!</source>
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source>
<target>Свързване със сървъра…</target>
@@ -1235,10 +1198,6 @@ This is your own one-time link!</source>
<target>Свързване със сървър…(грешка: %@)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve">
<source>Connection</source>
<target>Връзка</target>
@@ -1259,10 +1218,6 @@ This is your own one-time link!</source>
<target>Заявката за връзка е изпратена!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>Времето на изчакване за установяване на връзката изтече</target>
@@ -1342,10 +1297,6 @@ This is your own one-time link!</source>
<target>Създай SimpleX адрес</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>Създайте адрес, за да позволите на хората да се свързват с вас.</target>
@@ -1375,6 +1326,11 @@ This is your own one-time link!</source>
<target>Създайте нов профил в [настолното приложение](https://simplex.chat/downloads/). 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Създай линк за еднократна покана</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1399,10 +1355,6 @@ This is your own one-time link!</source>
<target>Създаден на %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>Текущ kод за достъп</target>
@@ -1749,18 +1701,6 @@ This cannot be undone!</source>
<target>Описание</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve">
<source>Develop</source>
<target>Разработване</target>
@@ -1851,19 +1791,11 @@ This cannot be undone!</source>
<target>Прекъсни връзката</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source>
<target>Открийте и се присъединете към групи</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve">
<source>Discover via local network</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
<source>Do NOT use SimpleX for emergency calls.</source>
<target>НЕ използвайте SimpleX за спешни повиквания.</target>
@@ -1944,10 +1876,6 @@ This cannot be undone!</source>
<target>Активиране на автоматично изтриване на съобщения?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>Активиране за всички</target>
@@ -2013,10 +1941,6 @@ This cannot be undone!</source>
<target>Криптирано съобщение или друго събитие</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>Криптирано съобщение: грешка в базата данни</target>
@@ -2042,14 +1966,6 @@ This cannot be undone!</source>
<target>Криптирано съобщение: неочаквана грешка</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source>
<target>Въведете kодa за достъп</target>
@@ -2079,10 +1995,6 @@ This cannot be undone!</source>
<target>Въведи сървъра ръчно</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source>
<target>Въведи съобщение при посрещане…</target>
@@ -2242,10 +2154,6 @@ This cannot be undone!</source>
<target>Грешка при зареждане на %@ сървъри</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Грешка при получаване на файл</target>
@@ -2286,10 +2194,6 @@ This cannot be undone!</source>
<target>Грешка при запазване на потребителска парола</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>Грешка при изпращане на имейл</target>
@@ -2414,10 +2318,6 @@ This cannot be undone!</source>
<target>Бързо и без чакане, докато подателят е онлайн!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Любим</target>
@@ -2513,10 +2413,6 @@ This cannot be undone!</source>
<target>За конзолата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve">
<source>French interface</source>
<target>Френски интерфейс</target>
@@ -2759,6 +2655,11 @@ This cannot be undone!</source>
<target>Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>Ако не можете да се срещнете на живо, можете да **сканирате QR код във видеообаждането** или вашият контакт може да сподели линк за покана.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>Ако въведете този kод за достъп, когато отваряте приложението, всички данни от приложението ще бъдат необратимо изтрити!</target>
@@ -2834,10 +2735,6 @@ This cannot be undone!</source>
<target>Инкогнито</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source>
<target>Режим инкогнито</target>
@@ -2868,10 +2765,6 @@ This cannot be undone!</source>
<target>Несъвместима версия на базата данни</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source>
<target>Неправилен kод за достъп</target>
@@ -2914,27 +2807,15 @@ This cannot be undone!</source>
<target>Интерфейс</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>Невалиден линк за връзка</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>Невалиден адрес на сървъра!</target>
@@ -3049,18 +2930,6 @@ This is your link for group %@!</source>
<target>Присъединяване към групата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>Запазете връзките си</target>
@@ -3121,18 +2990,6 @@ This is your link for group %@!</source>
<target>Ограничения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source>
<target>Съобщение на живо!</target>
@@ -3143,11 +3000,6 @@ This is your link for group %@!</source>
<target>Съобщения на живо</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>Локално</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>Локално име</target>
@@ -3387,10 +3239,6 @@ This is your link for group %@!</source>
<target>Нов kод за достъп</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>Нова заявка за контакт</target>
@@ -3491,10 +3339,6 @@ This is your link for group %@!</source>
<target>Няма получени или изпратени файлове</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Известия</target>
@@ -3514,15 +3358,16 @@ This is your link for group %@!</source>
- да деактивират членове (роля "наблюдател")</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>Изключено</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>Изключено (Локално)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ок</target>
@@ -3662,16 +3507,9 @@ This is your link for group %@!</source>
<target>Протокол и код с отворен код – всеки може да оперира собствени сървъри.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>Отваряне на база данни…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3714,8 +3552,9 @@ This is your link for group %@!</source>
<target>Парола за показване</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>Постави</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve">
@@ -3723,10 +3562,16 @@ This is your link for group %@!</source>
<target>Постави изображение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>Постави получения линк</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>Поставете линка, който сте получили, за да се свържете с вашия контакт.</target>
<note>placeholder</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
<target>Хората могат да се свържат с вас само чрез ликовете, които споделяте.</target>
@@ -3762,11 +3607,6 @@ This is your link for group %@!</source>
<target>Моля, проверете вашите настройки и тези вашия за контакт.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>Моля, свържете се с груповия администартор.</target>
@@ -3970,10 +3810,6 @@ Error: %@</source>
<target>Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
@@ -4182,10 +4018,6 @@ Error: %@</source>
<target>Грешка при възстановяване на базата данни</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>Покажи</target>
@@ -4316,10 +4148,6 @@ Error: %@</source>
<target>Сканирай QR код</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source>
<target>Сканирай код</target>
@@ -4340,10 +4168,6 @@ Error: %@</source>
<target>Търсене</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>Сигурна опашка</target>
@@ -4544,10 +4368,6 @@ Error: %@</source>
<target>Сървъри</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source>
<target>Задай 1 ден</target>
@@ -4618,8 +4438,9 @@ Error: %@</source>
<target>Сподели линк</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>Сподели линк за еднократна покана</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4742,15 +4563,16 @@ Error: %@</source>
<target>Някой</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>Започни нов чат</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>Започни чат</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>Започни миграция</target>
@@ -4875,14 +4697,6 @@ Error: %@</source>
<target>Докосни за инкогнито вход</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>Докосни за започване на нов чат</target>
@@ -4945,10 +4759,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Опитът за промяна на паролата на базата данни не беше завършен.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>Връзката, която приехте, ще бъде отказана!</target>
@@ -5014,10 +4824,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Сървърите за нови връзки на текущия ви чат профил **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Тема</target>
@@ -5058,10 +4864,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>Тази група има над %lld членове, потвърждения за доставка не се изпращат.</target>
@@ -5095,10 +4897,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>За да направите нова връзка</target>
@@ -5166,6 +4964,11 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>Изключи</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>Изключи известията?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>Включи</target>
@@ -5250,14 +5053,6 @@ To connect, please ask your contact to create another connection link and check
За да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source>
<target>Отключи</target>
@@ -5348,10 +5143,6 @@ To connect, please ask your contact to create another connection link and check
<target>Използвай за нови връзки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source>
<target>Използвай интерфейса за повикване на iOS</target>
@@ -5362,10 +5153,6 @@ To connect, please ask your contact to create another connection link and check
<target>Използвай нов инкогнито профил</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>Използвай сървър</target>
@@ -5386,23 +5173,11 @@ To connect, please ask your contact to create another connection link and check
<target>Използват се сървърите на SimpleX Chat.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source>
<target>Потвръди сигурността на връзката</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source>
<target>Потвръди кода за сигурност</target>
@@ -5413,10 +5188,6 @@ To connect, please ask your contact to create another connection link and check
<target>Чрез браузър</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve">
<source>Video call</source>
<target>Видео разговор</target>
@@ -5467,10 +5238,6 @@ To connect, please ask your contact to create another connection link and check
<target>Гласово съобщение…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source>
<target>Изчаква се получаването на файла</target>
@@ -5615,6 +5382,11 @@ Repeat join request?</source>
<target>Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона **Отваряне в мобилно приложение**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>Можете да го създадете по-късно</target>
@@ -5635,10 +5407,6 @@ Repeat join request?</source>
<target>Можете да скриете или заглушите известията за потребителски профил - плъзнете надясно.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Вече можете да изпращате съобщения до %@</target>
@@ -5679,10 +5447,6 @@ Repeat join request?</source>
<target>Можете да използвате markdown за форматиране на съобщенията:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>Не може да изпращате съобщения!</target>
@@ -5867,6 +5631,13 @@ You can cancel this connection and remove the contact (and try later with a new
<target>Вашите контакти могат да позволят пълното изтриване на съобщението.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>Вашите контакти в SimpleX ще го видят.
Можете да го промените в Настройки.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>Вашите контакти ще останат свързани.</target>
@@ -5997,10 +5768,6 @@ SimpleX сървърите не могат да видят вашия профи
<target>аудио разговор (не е e2e криптиран)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source>
<target>лошо ID на съобщението</target>
@@ -6581,10 +6348,6 @@ SimpleX сървърите не могат да видят вашия профи
<target>актуализиран профил на групата</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source>
<target>v%@ (%@)</target>
@@ -6722,10 +6485,6 @@ SimpleX сървърите не могат да видят вашия профи
<target>SimpleX използва Face ID за локалнa идентификация</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX се нуждае от достъп до микрофона за аудио и видео разговори и за запис на гласови съобщения.</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -290,30 +290,19 @@
<target>(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
<source>)</source>
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**Vytvořte odkaz / QR kód** pro váš kontakt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -326,6 +315,11 @@
<target>**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).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**Vložte přijatý odkaz** nebo jej otevřete v prohlížeči a klepněte na **Otevřít v mobilní aplikaci**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit.</target>
@@ -336,6 +330,11 @@
<target>**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.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>** Naskenujte QR kód**: pro připojení ke kontaktu osobně nebo prostřednictvím videohovoru.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence.</target>
@@ -379,12 +378,6 @@
- a více!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -433,6 +426,11 @@
<target>1 týden</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>Jednorázový odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 minut</target>
@@ -548,10 +546,6 @@
<target>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.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>Přidejte přednastavené servery</target>
@@ -864,10 +858,6 @@
<target>Zpět</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source>
<target>Špatné ID zprávy</target>
@@ -878,10 +868,6 @@
<target>Špatný hash zprávy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Lepší zprávy</target>
@@ -891,10 +877,6 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
@@ -948,10 +930,6 @@
<target>Hovory</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>Nemohu smazat uživatelský profil!</target>
@@ -1068,10 +1046,6 @@
<target>Chat je zastaven</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>Předvolby chatu</target>
@@ -1172,19 +1146,11 @@
<target>Připojit</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve">
<source>Connect automatically</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>Spojit se inkognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve">
<source>Connect to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve">
<source>Connect to yourself?</source>
<note>No comment provided by engineer.</note>
@@ -1208,6 +1174,11 @@ This is your own one-time link!</source>
<target>Připojte se prostřednictvím odkazu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>Připojit se prostřednictvím odkazu / QR kódu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>Připojit se jednorázovým odkazem</target>
@@ -1217,14 +1188,6 @@ This is your own one-time link!</source>
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source>
<target>Připojování k serveru…</target>
@@ -1235,10 +1198,6 @@ This is your own one-time link!</source>
<target>Připojování k serveru... (chyba: %@)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve">
<source>Connection</source>
<target>Připojení</target>
@@ -1259,10 +1218,6 @@ This is your own one-time link!</source>
<target>Požadavek na připojení byl odeslán!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>Časový limit připojení</target>
@@ -1342,10 +1297,6 @@ This is your own one-time link!</source>
<target>Vytvořit SimpleX adresu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>Vytvořit adresu, aby se s vámi lidé mohli spojit.</target>
@@ -1375,6 +1326,11 @@ This is your own one-time link!</source>
<target>Vytvořit nový profil v [desktop app](https://simplex.chat/downloads/). 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Vytvořit jednorázovou pozvánku</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1399,10 +1355,6 @@ This is your own one-time link!</source>
<target>Vytvořeno na %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>Aktuální heslo</target>
@@ -1749,18 +1701,6 @@ This cannot be undone!</source>
<target>Popis</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve">
<source>Develop</source>
<target>Vyvinout</target>
@@ -1851,19 +1791,11 @@ This cannot be undone!</source>
<target>Odpojit</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source>
<target>Objevte a připojte skupiny</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve">
<source>Discover via local network</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
<source>Do NOT use SimpleX for emergency calls.</source>
<target>NEpoužívejte SimpleX pro tísňová volání.</target>
@@ -1944,10 +1876,6 @@ This cannot be undone!</source>
<target>Povolit automatické mazání zpráv?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>Povolit pro všechny</target>
@@ -2013,10 +1941,6 @@ This cannot be undone!</source>
<target>Šifrovaná zpráva nebo jiná událost</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>Šifrovaná zpráva: chyba databáze</target>
@@ -2042,14 +1966,6 @@ This cannot be undone!</source>
<target>Šifrovaná zpráva: neočekávaná chyba</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source>
<target>Zadat heslo</target>
@@ -2079,10 +1995,6 @@ This cannot be undone!</source>
<target>Zadejte server ručně</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source>
<target>Zadat uvítací zprávu…</target>
@@ -2242,10 +2154,6 @@ This cannot be undone!</source>
<target>Chyba načítání %@ serverů</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Chyba při příjmu souboru</target>
@@ -2286,10 +2194,6 @@ This cannot be undone!</source>
<target>Chyba ukládání hesla uživatele</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>Chyba odesílání e-mailu</target>
@@ -2414,10 +2318,6 @@ This cannot be undone!</source>
<target>Rychle a bez čekání, než bude odesílatel online!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Oblíbené</target>
@@ -2513,10 +2413,6 @@ This cannot be undone!</source>
<target>Pro konzoli</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve">
<source>French interface</source>
<target>Francouzské rozhraní</target>
@@ -2759,6 +2655,11 @@ This cannot be undone!</source>
<target>Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>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.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána!</target>
@@ -2834,10 +2735,6 @@ This cannot be undone!</source>
<target>Inkognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source>
<target>Režim inkognito</target>
@@ -2868,10 +2765,6 @@ This cannot be undone!</source>
<target>Nekompatibilní verze databáze</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source>
<target>Nesprávné heslo</target>
@@ -2914,27 +2807,15 @@ This cannot be undone!</source>
<target>Rozhranní</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>Neplatný odkaz na spojení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>Neplatná adresa serveru!</target>
@@ -3049,18 +2930,6 @@ This is your link for group %@!</source>
<target>Připojování ke skupině</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>Zachovat vaše připojení</target>
@@ -3121,18 +2990,6 @@ This is your link for group %@!</source>
<target>Omezení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source>
<target>Živé zprávy!</target>
@@ -3143,11 +3000,6 @@ This is your link for group %@!</source>
<target>Živé zprávy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>Místní</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>Místní název</target>
@@ -3387,10 +3239,6 @@ This is your link for group %@!</source>
<target>Nové heslo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>Žádost o nový kontakt</target>
@@ -3491,10 +3339,6 @@ This is your link for group %@!</source>
<target>Žádné přijaté ani odeslané soubory</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Oznámení</target>
@@ -3514,15 +3358,16 @@ This is your link for group %@!</source>
- zakázat členy (role "pozorovatel")</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>Vypnout</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>Vypnuto (místní)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
@@ -3662,16 +3507,9 @@ This is your link for group %@!</source>
<target>Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>Otvírání databáze…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3714,8 +3552,9 @@ This is your link for group %@!</source>
<target>Heslo k zobrazení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>Vložit</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve">
@@ -3723,10 +3562,16 @@ This is your link for group %@!</source>
<target>Vložit obrázek</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>Vložení přijatého odkazu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem.</target>
<note>placeholder</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
<target>Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte.</target>
@@ -3762,11 +3607,6 @@ This is your link for group %@!</source>
<target>Zkontrolujte prosím nastavení své i svého kontaktu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>Kontaktujte prosím správce skupiny.</target>
@@ -3970,10 +3810,6 @@ Error: %@</source>
<target>Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>Přečtěte si více v [Uživatelské příručce](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
@@ -4182,10 +4018,6 @@ Error: %@</source>
<target>Chyba obnovení databáze</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>Odhalit</target>
@@ -4316,10 +4148,6 @@ Error: %@</source>
<target>Skenovat QR kód</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source>
<target>Skenovat kód</target>
@@ -4340,10 +4168,6 @@ Error: %@</source>
<target>Hledat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>Zabezpečit frontu</target>
@@ -4544,10 +4368,6 @@ Error: %@</source>
<target>Servery</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source>
<target>Nastavit 1 den</target>
@@ -4618,8 +4438,9 @@ Error: %@</source>
<target>Sdílet odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>Jednorázový zvací odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4742,15 +4563,16 @@ Error: %@</source>
<target>Někdo</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>Začít nový chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>Začít chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>Zahájit přenesení</target>
@@ -4875,14 +4697,6 @@ Error: %@</source>
<target>Klepnutím se připojíte inkognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>Klepnutím na zahájíte nový chat</target>
@@ -4945,10 +4759,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<target>Pokus o změnu přístupové fráze databáze nebyl dokončen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>Připojení, které jste přijali, bude zrušeno!</target>
@@ -5014,10 +4824,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<target>Servery pro nová připojení vašeho aktuálního chat profilu **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Téma</target>
@@ -5058,10 +4864,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<target>Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>Tato skupina má více než %lld členů, potvrzení o doručení nejsou odesílány.</target>
@@ -5095,10 +4897,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<target>Pro připojení může váš kontakt naskenovat QR kód, nebo použít odkaz v aplikaci.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>Vytvoření nového připojení</target>
@@ -5166,6 +4964,11 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
<target>Vypnout</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>Vypnout upozornění?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>Zapnout</target>
@@ -5250,14 +5053,6 @@ To connect, please ask your contact to create another connection link and check
Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source>
<target>Odemknout</target>
@@ -5348,10 +5143,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Použít pro nová připojení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source>
<target>Použít rozhraní volání iOS</target>
@@ -5362,10 +5153,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Použít nový inkognito profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>Použít server</target>
@@ -5386,23 +5173,11 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Používat servery SimpleX Chat.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source>
<target>Ověření zabezpečení připojení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source>
<target>Ověření bezpečnostního kódu</target>
@@ -5413,10 +5188,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Prostřednictvím prohlížeče</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve">
<source>Video call</source>
<target>Videohovor</target>
@@ -5467,10 +5238,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Hlasová zpráva…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source>
<target>Čekání na soubor</target>
@@ -5615,6 +5382,11 @@ Repeat join request?</source>
<target>Můžete přijímat hovory z obrazovky zámku, bez ověření zařízení a aplikace.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>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**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>Můžete vytvořit později</target>
@@ -5635,10 +5407,6 @@ Repeat join request?</source>
<target>Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Nyní můžete posílat zprávy %@</target>
@@ -5679,10 +5447,6 @@ Repeat join request?</source>
<target>K formátování zpráv můžete použít markdown:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>Nemůžete posílat zprávy!</target>
@@ -5867,6 +5631,13 @@ Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s nov
<target>Vaše kontakty mohou povolit úplné mazání zpráv.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>Vaše kontakty v SimpleX ji uvidí.
Můžete ji změnit v Nastavení.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>Vaše kontakty zůstanou připojeny.</target>
@@ -5997,10 +5768,6 @@ Servery SimpleX nevidí váš profil.</target>
<target>zvukový hovor (nešifrovaný e2e)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source>
<target>špatné ID zprávy</target>
@@ -6580,10 +6347,6 @@ Servery SimpleX nevidí váš profil.</target>
<target>aktualizoval profil skupiny</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source>
<target>v%@ (%@)</target>
@@ -6721,10 +6484,6 @@ Servery SimpleX nevidí váš profil.</target>
<target>SimpleX používá Face ID pro místní ověřování</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX potřebuje přístup k mikrofonu pro audio a video hovory a pro nahrávání hlasových zpráv.</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -31,39 +31,32 @@ Available in v5.1</source>
<source> (</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" (can be copied)" xml:space="preserve" approved="no">
<trans-unit id=" (can be copied)" xml:space="preserve">
<source> (can be copied)</source>
<target state="translated"> (μπορεί να αντιγραφή)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="!1 colored!" xml:space="preserve" approved="no">
<trans-unit id="!1 colored!" xml:space="preserve">
<source>!1 colored!</source>
<target state="translated">!1 έγχρωμο!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="#secret#" xml:space="preserve" approved="no">
<trans-unit id="#secret#" xml:space="preserve">
<source>#secret#</source>
<target state="translated">#μυστικό#</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@" xml:space="preserve" approved="no">
<trans-unit id="%@" xml:space="preserve">
<source>%@</source>
<target state="translated">%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ %@" xml:space="preserve" approved="no">
<trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source>
<target state="translated">%@ %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ / %@" xml:space="preserve" approved="no">
<trans-unit id="%@ / %@" xml:space="preserve">
<source>%@ / %@</source>
<target state="translated">%@ / %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve" approved="no">
<trans-unit id="%@ is connected!" xml:space="preserve">
<source>%@ is connected!</source>
<target state="translated">%@ είναι συνδεδεμένο!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@ is not verified" xml:space="preserve">
@@ -4169,51 +4162,6 @@ SimpleX servers cannot see your profile.</source>
<source>\~strike~</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ connected" xml:space="preserve" approved="no">
<source>%@ connected</source>
<target state="translated">%@ συνδεδεμένο</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="translated"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve" approved="no">
<source>%@ and %@</source>
<target state="translated">%@ και %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ at %@:" xml:space="preserve" approved="no">
<source>%1$@ at %2$@:</source>
<target state="translated">%1$@ στις %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve" approved="no">
<source>## History</source>
<target state="translated">## Ιστορικό</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
<source>## In reply to</source>
<target state="translated">## Ως απαντηση σε</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ (current)" xml:space="preserve" approved="no">
<source>%@ (current)</source>
<target state="translated">%@ (τωρινό)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ (current):" xml:space="preserve" approved="no">
<source>%@ (current):</source>
<target state="translated">%@ (τωρινό):</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
<source>%@ and %@ connected</source>
<target state="translated">%@ και %@ συνδεδεμένο</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="el" datatype="plaintext">
@@ -297,34 +297,19 @@
<target>(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<target>(new)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<target>(this device v%@)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
<source>)</source>
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<target>**Add contact**: to create a new invitation link, or connect via a link you received.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**Add new contact**: to create your one-time QR Code or link for your contact.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<target>**Create group**: to create a new group.</target>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**Create link / QR code** for your contact to use.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -337,6 +322,11 @@
<target>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**Paste received link** or open it in the browser and tap **Open in mobile app**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</target>
@@ -347,6 +337,11 @@
<target>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**Scan QR code**: to connect to your contact in person or via video call.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**Warning**: Instant push notifications require passphrase saved in Keychain.</target>
@@ -387,15 +382,6 @@
- and more!</source>
<target>- more stable message delivery.
- a bit better groups.
- and more!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<target>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -448,6 +434,11 @@
<target>1 week</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>1-time link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 minutes</target>
@@ -563,11 +554,6 @@
<target>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<target>Add contact</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>Add preset servers</target>
@@ -883,11 +869,6 @@
<target>Back</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<target>Bad desktop address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source>
<target>Bad message ID</target>
@@ -898,11 +879,6 @@
<target>Bad message hash</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>Better groups</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Better messages</target>
@@ -913,11 +889,6 @@
<target>Block</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<target>Block group members</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<target>Block member</target>
@@ -973,11 +944,6 @@
<target>Calls</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<target>Camera not available</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>Can't delete user profile!</target>
@@ -1094,11 +1060,6 @@
<target>Chat is stopped</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<target>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>Chat preferences</target>
@@ -1199,21 +1160,11 @@
<target>Connect</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve">
<source>Connect automatically</source>
<target>Connect automatically</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>Connect incognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve">
<source>Connect to desktop</source>
<target>Connect to desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve">
<source>Connect to yourself?</source>
<target>Connect to yourself?</target>
@@ -1243,6 +1194,11 @@ This is your own one-time link!</target>
<target>Connect via link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>Connect via link / QR code</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>Connect via one-time link</target>
@@ -1253,16 +1209,6 @@ This is your own one-time link!</target>
<target>Connect with %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<target>Connected desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<target>Connected to desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source>
<target>Connecting to server…</target>
@@ -1273,11 +1219,6 @@ This is your own one-time link!</target>
<target>Connecting to server… (error: %@)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<target>Connecting to desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve">
<source>Connection</source>
<target>Connection</target>
@@ -1298,11 +1239,6 @@ This is your own one-time link!</target>
<target>Connection request sent!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<target>Connection terminated</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>Connection timeout</target>
@@ -1383,11 +1319,6 @@ This is your own one-time link!</target>
<target>Create SimpleX address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<target>Create a group using a random profile.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>Create an address to let people connect with you.</target>
@@ -1418,6 +1349,11 @@ This is your own one-time link!</target>
<target>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Create one-time invitation link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<target>Create profile</target>
@@ -1443,11 +1379,6 @@ This is your own one-time link!</target>
<target>Created on %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<target>Creating link…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>Current Passcode</target>
@@ -1798,21 +1729,6 @@ This cannot be undone!</target>
<target>Description</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<target>Desktop address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<target>Desktop app version %@ is not compatible with this app.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<target>Desktop devices</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve">
<source>Develop</source>
<target>Develop</target>
@@ -1903,21 +1819,11 @@ This cannot be undone!</target>
<target>Disconnect</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<target>Disconnect desktop?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source>
<target>Discover and join groups</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve">
<source>Discover via local network</source>
<target>Discover via local network</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
<source>Do NOT use SimpleX for emergency calls.</source>
<target>Do NOT use SimpleX for emergency calls.</target>
@@ -1998,11 +1904,6 @@ This cannot be undone!</target>
<target>Enable automatic message deletion?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<target>Enable camera access</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>Enable for all</target>
@@ -2068,11 +1969,6 @@ This cannot be undone!</target>
<target>Encrypted message or another event</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<target>Encrypted message: app is stopped</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>Encrypted message: database error</target>
@@ -2098,16 +1994,6 @@ This cannot be undone!</target>
<target>Encrypted message: unexpected error</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<target>Encryption re-negotiation error</target>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<target>Encryption re-negotiation failed.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source>
<target>Enter Passcode</target>
@@ -2138,11 +2024,6 @@ This cannot be undone!</target>
<target>Enter server manually</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<target>Enter this device name…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source>
<target>Enter welcome message…</target>
@@ -2303,11 +2184,6 @@ This cannot be undone!</target>
<target>Error loading %@ servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Error opening chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Error receiving file</target>
@@ -2348,11 +2224,6 @@ This cannot be undone!</target>
<target>Error saving user password</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<target>Error scanning code: %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>Error sending email</target>
@@ -2478,11 +2349,6 @@ This cannot be undone!</target>
<target>Fast and no wait until the sender is online!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<target>Faster joining and more reliable messages.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Favorite</target>
@@ -2578,11 +2444,6 @@ This cannot be undone!</target>
<target>For console</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<target>Found desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve">
<source>French interface</source>
<target>French interface</target>
@@ -2828,6 +2689,11 @@ This cannot be undone!</target>
<target>If you can't meet in person, show QR code in a video call, or share the link.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>If you enter this passcode when opening the app, all app data will be irreversibly removed!</target>
@@ -2903,11 +2769,6 @@ This cannot be undone!</target>
<target>Incognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<target>Incognito groups</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source>
<target>Incognito mode</target>
@@ -2938,11 +2799,6 @@ This cannot be undone!</target>
<target>Incompatible database version</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<target>Incompatible version</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source>
<target>Incorrect passcode</target>
@@ -2985,31 +2841,16 @@ This cannot be undone!</target>
<target>Interface</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<target>Invalid QR code</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>Invalid connection link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Invalid link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<target>Invalid name!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<target>Invalid response</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>Invalid server address!</target>
@@ -3128,21 +2969,6 @@ This is your link for group %@!</target>
<target>Joining group</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<target>Keep</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<target>Keep the app open to use it from desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<target>Keep unused invitation?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>Keep your connections</target>
@@ -3203,21 +3029,6 @@ This is your link for group %@!</target>
<target>Limitations</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<target>Link mobile and desktop apps! 🔗</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<target>Linked desktop options</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<target>Linked desktops</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source>
<target>Live message!</target>
@@ -3228,11 +3039,6 @@ This is your link for group %@!</target>
<target>Live messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>Local</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>Local name</target>
@@ -3473,11 +3279,6 @@ This is your link for group %@!</target>
<target>New Passcode</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>New chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>New contact request</target>
@@ -3578,11 +3379,6 @@ This is your link for group %@!</target>
<target>No received or sent files</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<target>Not compatible!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Notifications</target>
@@ -3602,16 +3398,16 @@ This is your link for group %@!</target>
- disable members ("observer" role)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<target>OK</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>Off</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>Off (Local)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
@@ -3752,19 +3548,9 @@ This is your link for group %@!</target>
<target>Open-source protocol and code anybody can run the servers.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<target>Opening app…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<target>Or scan QR code</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<target>Or show this code</target>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>Opening database…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3807,9 +3593,9 @@ This is your link for group %@!</target>
<target>Password to show</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<target>Paste desktop address</target>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>Paste</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve">
@@ -3817,11 +3603,16 @@ This is your link for group %@!</target>
<target>Paste image</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<target>Paste the link you received</target>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>Paste received link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>Paste the link you received to connect with your contact.</target>
<note>placeholder</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
<target>People can connect to you only via the links you share.</target>
@@ -3857,13 +3648,6 @@ This is your link for group %@!</target>
<target>Please check yours and your contact preferences.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<target>Please contact developers.
Error: %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>Please contact group admin.</target>
@@ -4069,11 +3853,6 @@ Error: %@</target>
<target>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<target>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
@@ -4284,11 +4063,6 @@ Error: %@</target>
<target>Restore database error</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<target>Retry</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>Reveal</target>
@@ -4419,11 +4193,6 @@ Error: %@</target>
<target>Scan QR code</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<target>Scan QR code from desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source>
<target>Scan code</target>
@@ -4444,11 +4213,6 @@ Error: %@</target>
<target>Search</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Search or paste SimpleX link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>Secure queue</target>
@@ -4649,11 +4413,6 @@ Error: %@</target>
<target>Servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<target>Session code</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source>
<target>Set 1 day</target>
@@ -4724,9 +4483,9 @@ Error: %@</target>
<target>Share link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Share this 1-time invite link</target>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>Share one-time invitation link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4849,16 +4608,16 @@ Error: %@</target>
<target>Somebody</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>Start a new chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>Start chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<target>Start chat?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>Start migration</target>
@@ -4984,16 +4743,6 @@ Error: %@</target>
<target>Tap to join incognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<target>Tap to paste link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<target>Tap to scan</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>Tap to start a new chat</target>
@@ -5056,11 +4805,6 @@ It can happen because of some bug or when the connection is compromised.</target
<target>The attempt to change database passphrase was not completed.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<target>The code you scanned is not a SimpleX link QR code.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>The connection you accepted will be cancelled!</target>
@@ -5126,11 +4870,6 @@ It can happen because of some bug or when the connection is compromised.</target
<target>The servers for new connections of your current chat profile **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<target>The text you pasted is not a SimpleX link.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Theme</target>
@@ -5171,11 +4910,6 @@ It can happen because of some bug or when the connection is compromised.</target
<target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<target>This device name</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>This group has over %lld members, delivery receipts are not sent.</target>
@@ -5211,11 +4945,6 @@ It can happen because of some bug or when the connection is compromised.</target
<target>To connect, your contact can scan QR code or use the link in the app.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<target>To hide unwanted messages.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>To make a new connection</target>
@@ -5283,6 +5012,11 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>Turn off</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>Turn off notifications?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>Turn on</target>
@@ -5370,16 +5104,6 @@ To connect, please ask your contact to create another connection link and check
To connect, please ask your contact to create another connection link and check that you have a stable network connection.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<target>Unlink</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<target>Unlink desktop?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source>
<target>Unlock</target>
@@ -5470,11 +5194,6 @@ To connect, please ask your contact to create another connection link and check
<target>Use for new connections</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<target>Use from desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source>
<target>Use iOS call interface</target>
@@ -5485,11 +5204,6 @@ To connect, please ask your contact to create another connection link and check
<target>Use new incognito profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<target>Use only local notifications?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>Use server</target>
@@ -5510,26 +5224,11 @@ To connect, please ask your contact to create another connection link and check
<target>Using SimpleX Chat servers.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<target>Verify code with desktop</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<target>Verify connection</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source>
<target>Verify connection security</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<target>Verify connections</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source>
<target>Verify security code</target>
@@ -5540,11 +5239,6 @@ To connect, please ask your contact to create another connection link and check
<target>Via browser</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<target>Via secure quantum resistant protocol.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve">
<source>Video call</source>
<target>Video call</target>
@@ -5595,11 +5289,6 @@ To connect, please ask your contact to create another connection link and check
<target>Voice message…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<target>Waiting for desktop...</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source>
<target>Waiting for file</target>
@@ -5752,6 +5441,11 @@ Repeat join request?</target>
<target>You can accept calls from lock screen, without device and app authentication.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>You can create it later</target>
@@ -5772,11 +5466,6 @@ Repeat join request?</target>
<target>You can hide or mute a user profile - swipe it to the right.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<target>You can make it visible to your SimpleX contacts via Settings.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>You can now send messages to %@</target>
@@ -5817,11 +5506,6 @@ Repeat join request?</target>
<target>You can use markdown to format messages:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<target>You can view invitation link again in connection details.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>You can't send messages!</target>
@@ -6011,6 +5695,13 @@ You can cancel this connection and remove the contact (and try later with a new
<target>Your contacts can allow full message deletion.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>Your contacts in SimpleX will see it.
You can change it in Settings.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>Your contacts will remain connected.</target>
@@ -6143,11 +5834,6 @@ SimpleX servers cannot see your profile.</target>
<target>audio call (not e2e encrypted)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<target>author</target>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source>
<target>bad message ID</target>
@@ -6730,11 +6416,6 @@ SimpleX servers cannot see your profile.</target>
<target>updated group profile</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<target>v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source>
<target>v%@ (%@)</target>
@@ -6872,11 +6553,6 @@ SimpleX servers cannot see your profile.</target>
<target>SimpleX uses Face ID for local authentication</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<target>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</target>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX needs microphone access for audio and video calls, and to record voice messages.</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -290,30 +290,19 @@
<target>(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
<source>)</source>
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**Luo linkki / QR-koodi* kontaktille.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -326,6 +315,11 @@
<target>**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen.</target>
@@ -336,6 +330,11 @@
<target>**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin.</target>
@@ -376,12 +375,6 @@
- ja paljon muuta!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -430,6 +423,11 @@
<target>1 viikko</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>Kertakäyttölinkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 minuuttia</target>
@@ -545,10 +543,6 @@
<target>Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>Lisää esiasetettuja palvelimia</target>
@@ -860,10 +854,6 @@
<target>Takaisin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source>
<target>Virheellinen viestin tunniste</target>
@@ -874,10 +864,6 @@
<target>Virheellinen viestin tarkiste</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Parempia viestejä</target>
@@ -887,10 +873,6 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
@@ -943,10 +925,6 @@
<target>Puhelut</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>Käyttäjäprofiilia ei voi poistaa!</target>
@@ -1063,10 +1041,6 @@
<target>Chat on pysäytetty</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>Chat-asetukset</target>
@@ -1167,19 +1141,11 @@
<target>Yhdistä</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve">
<source>Connect automatically</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>Yhdistä Incognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve">
<source>Connect to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve">
<source>Connect to yourself?</source>
<note>No comment provided by engineer.</note>
@@ -1203,6 +1169,11 @@ This is your own one-time link!</source>
<target>Yhdistä linkin kautta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>Yhdistä linkillä / QR-koodilla</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>Yhdistä kertalinkillä</target>
@@ -1212,14 +1183,6 @@ This is your own one-time link!</source>
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source>
<target>Yhteyden muodostaminen palvelimeen…</target>
@@ -1230,10 +1193,6 @@ This is your own one-time link!</source>
<target>Yhteyden muodostaminen palvelimeen... (virhe: %@)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve">
<source>Connection</source>
<target>Yhteys</target>
@@ -1254,10 +1213,6 @@ This is your own one-time link!</source>
<target>Yhteyspyyntö lähetetty!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>Yhteyden aikakatkaisu</target>
@@ -1337,10 +1292,6 @@ This is your own one-time link!</source>
<target>Luo SimpleX-osoite</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä.</target>
@@ -1370,6 +1321,11 @@ This is your own one-time link!</source>
<target>Luo uusi profiili [työpöytäsovelluksessa](https://simplex.chat/downloads/). 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Luo kertakutsulinkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1394,10 +1350,6 @@ This is your own one-time link!</source>
<target>Luotu %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>Nykyinen pääsykoodi</target>
@@ -1744,18 +1696,6 @@ This cannot be undone!</source>
<target>Kuvaus</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve">
<source>Develop</source>
<target>Kehitä</target>
@@ -1846,19 +1786,11 @@ This cannot be undone!</source>
<target>Katkaise</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source>
<target>Löydä ryhmiä ja liity niihin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve">
<source>Discover via local network</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
<source>Do NOT use SimpleX for emergency calls.</source>
<target>Älä käytä SimpleX-sovellusta hätäpuheluihin.</target>
@@ -1939,10 +1871,6 @@ This cannot be undone!</source>
<target>Ota automaattinen viestien poisto käyttöön?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>Salli kaikille</target>
@@ -2007,10 +1935,6 @@ This cannot be undone!</source>
<target>Salattu viesti tai muu tapahtuma</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>Salattu viesti: tietokantavirhe</target>
@@ -2036,14 +1960,6 @@ This cannot be undone!</source>
<target>Salattu viesti: odottamaton virhe</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source>
<target>Syötä pääsykoodi</target>
@@ -2073,10 +1989,6 @@ This cannot be undone!</source>
<target>Syötä palvelin manuaalisesti</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source>
<target>Kirjoita tervetuloviesti…</target>
@@ -2235,10 +2147,6 @@ This cannot be undone!</source>
<target>Virhe %@-palvelimien lataamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Virhe tiedoston vastaanottamisessa</target>
@@ -2279,10 +2187,6 @@ This cannot be undone!</source>
<target>Virhe käyttäjän salasanan tallentamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>Virhe sähköpostin lähettämisessä</target>
@@ -2406,10 +2310,6 @@ This cannot be undone!</source>
<target>Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>Suosikki</target>
@@ -2505,10 +2405,6 @@ This cannot be undone!</source>
<target>Konsoliin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve">
<source>French interface</source>
<target>Ranskalainen käyttöliittymä</target>
@@ -2751,6 +2647,11 @@ This cannot be undone!</source>
<target>Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>Jos et voi tavata henkilökohtaisesti, voit **skannata QR-koodin videopuhelussa** tai kontaktisi voi jakaa kutsulinkin.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>Jos syötät tämän pääsykoodin sovellusta avatessasi, kaikki sovelluksen tiedot poistetaan peruuttamattomasti!</target>
@@ -2826,10 +2727,6 @@ This cannot be undone!</source>
<target>Incognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source>
<target>Incognito-tila</target>
@@ -2860,10 +2757,6 @@ This cannot be undone!</source>
<target>Yhteensopimaton tietokantaversio</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source>
<target>Väärä pääsykoodi</target>
@@ -2906,27 +2799,15 @@ This cannot be undone!</source>
<target>Käyttöliittymä</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>Virheellinen yhteyslinkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>Virheellinen palvelinosoite!</target>
@@ -3041,18 +2922,6 @@ This is your link for group %@!</source>
<target>Liittyy ryhmään</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>Pidä kontaktisi</target>
@@ -3113,18 +2982,6 @@ This is your link for group %@!</source>
<target>Rajoitukset</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source>
<target>Live-viesti!</target>
@@ -3135,11 +2992,6 @@ This is your link for group %@!</source>
<target>Live-viestit</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>Paikallinen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>Paikallinen nimi</target>
@@ -3379,10 +3231,6 @@ This is your link for group %@!</source>
<target>Uusi pääsykoodi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>Uusi kontaktipyyntö</target>
@@ -3482,10 +3330,6 @@ This is your link for group %@!</source>
<target>Ei vastaanotettuja tai lähetettyjä tiedostoja</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Ilmoitukset</target>
@@ -3505,15 +3349,16 @@ This is your link for group %@!</source>
- poista jäsenet käytöstä ("tarkkailija" rooli)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>Pois</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>Pois (Paikallinen)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
@@ -3652,16 +3497,9 @@ This is your link for group %@!</source>
<target>Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>Avataan tietokantaa…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3704,8 +3542,9 @@ This is your link for group %@!</source>
<target>Salasana näytettäväksi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>Liitä</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve">
@@ -3713,10 +3552,16 @@ This is your link for group %@!</source>
<target>Liitä kuva</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>Liitä vastaanotettu linkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>Liitä saamasi linkki, jonka avulla voit muodostaa yhteyden kontaktiisi.</target>
<note>placeholder</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
<target>Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta.</target>
@@ -3752,11 +3597,6 @@ This is your link for group %@!</source>
<target>Tarkista omasi ja kontaktin asetukset.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>Ota yhteyttä ryhmän ylläpitäjään.</target>
@@ -3960,10 +3800,6 @@ Error: %@</source>
<target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
@@ -4172,10 +4008,6 @@ Error: %@</source>
<target>Virhe tietokannan palauttamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>Paljasta</target>
@@ -4306,10 +4138,6 @@ Error: %@</source>
<target>Skannaa QR-koodi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source>
<target>Skannaa koodi</target>
@@ -4330,10 +4158,6 @@ Error: %@</source>
<target>Haku</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>Turvallinen jono</target>
@@ -4533,10 +4357,6 @@ Error: %@</source>
<target>Palvelimet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source>
<target>Aseta 1 päivä</target>
@@ -4607,8 +4427,9 @@ Error: %@</source>
<target>Jaa linkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>Jaa kertakutsulinkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4730,15 +4551,16 @@ Error: %@</source>
<target>Joku</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>Aloita uusi keskustelu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>Aloita keskustelu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>Aloita siirto</target>
@@ -4863,14 +4685,6 @@ Error: %@</source>
<target>Napauta liittyäksesi incognito-tilassa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>Aloita uusi keskustelu napauttamalla</target>
@@ -4933,10 +4747,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>Hyväksymäsi yhteys peruuntuu!</target>
@@ -5002,10 +4812,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>Teema</target>
@@ -5046,10 +4852,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä.</target>
@@ -5083,10 +4885,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<target>Kontaktisi voi muodostaa yhteyden skannaamalla QR-koodin tai käyttämällä sovelluksessa olevaa linkkiä.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>Uuden yhteyden luominen</target>
@@ -5153,6 +4951,11 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote
<target>Sammuta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>Kytke ilmoitukset pois päältä?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>Kytke päälle</target>
@@ -5237,14 +5040,6 @@ To connect, please ask your contact to create another connection link and check
Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source>
<target>Avaa</target>
@@ -5335,10 +5130,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Käytä uusiin yhteyksiin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source>
<target>Käytä iOS:n puhelujen käyttöliittymää</target>
@@ -5349,10 +5140,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Käytä uutta incognito-profiilia</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>Käytä palvelinta</target>
@@ -5373,23 +5160,11 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Käyttää SimpleX Chat -palvelimia.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source>
<target>Tarkista yhteyden suojaus</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source>
<target>Tarkista turvakoodi</target>
@@ -5400,10 +5175,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Selaimella</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve">
<source>Video call</source>
<target>Videopuhelu</target>
@@ -5454,10 +5225,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Ääniviesti…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source>
<target>Odottaa tiedostoa</target>
@@ -5602,6 +5369,11 @@ Repeat join request?</source>
<target>Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>Voit myös muodostaa yhteyden klikkaamalla linkkiä. Jos se avautuu selaimessa, napsauta **Avaa mobiilisovelluksessa**-painiketta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>Voit luoda sen myöhemmin</target>
@@ -5622,10 +5394,6 @@ Repeat join request?</source>
<target>Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>Voit nyt lähettää viestejä %@:lle</target>
@@ -5666,10 +5434,6 @@ Repeat join request?</source>
<target>Voit käyttää markdownia viestien muotoiluun:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>Et voi lähettää viestejä!</target>
@@ -5854,6 +5618,13 @@ Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uu
<target>Kontaktisi voivat sallia viestien täydellisen poistamisen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>Kontaktisi SimpleX:ssä näkevät sen.
Voit muuttaa sitä Asetuksista.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>Kontaktisi pysyvät yhdistettyinä.</target>
@@ -5984,10 +5755,6 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>äänipuhelu (ei e2e-salattu)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source>
<target>virheellinen viestin tunniste</target>
@@ -6566,10 +6333,6 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>päivitetty ryhmäprofiili</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source>
<target>v%@ (%@)</target>
@@ -6707,10 +6470,6 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
<target>SimpleX käyttää Face ID:tä paikalliseen todennukseen</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX tarvitsee mikrofonia ääni- ja videopuheluita ja ääniviestien tallentamista varten.</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -103,7 +103,6 @@
</trans-unit>
<trans-unit id="%@ connected" xml:space="preserve">
<source>%@ connected</source>
<target>%@ 接続中</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ is connected!" xml:space="preserve">
@@ -215,12 +214,10 @@
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
<source>%lld messages marked deleted</source>
<target>%lld 件のメッセージが削除されました</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages moderated by %@" xml:space="preserve">
<source>%lld messages moderated by %@</source>
<target>%@ により%lld 件のメッセージが検閲されました</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld minutes" xml:space="preserve">
@@ -293,30 +290,19 @@
<target>(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
<source>)</source>
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**新しい連絡先を追加**: 連絡先のワンタイム QR コードまたはリンクを作成します。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>連絡先が使用する **リンク/QR コードを作成します**。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -329,6 +315,11 @@
<target>**最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**受信したリンク**を貼り付けるか、ブラウザーで開いて [**モバイル アプリで開く**] をタップします。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**注意**: パスフレーズを紛失すると、パスフレーズを復元または変更できなくなります。</target>
@@ -339,6 +330,11 @@
<target>**推奨**: デバイス トークンと通知は SimpleX Chat 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**QR コードをスキャン**: 直接またはビデオ通話で連絡先に接続します。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。</target>
@@ -379,12 +375,6 @@
- などなど!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -433,6 +423,11 @@
<target>1週間</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>使い捨てのリンク</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5分</target>
@@ -548,10 +543,6 @@
<target>プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>既存サーバを追加</target>
@@ -864,10 +855,6 @@
<target>戻る</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source>
<target>メッセージ ID が正しくありません</target>
@@ -878,10 +865,6 @@
<target>メッセージのハッシュ値問題</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>より良いメッセージ</target>
@@ -891,10 +874,6 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
@@ -948,10 +927,6 @@
<target>通話</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>ユーザープロフィールが削除できません!</target>
@@ -1068,10 +1043,6 @@
<target>チャットが停止してます</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>チャット設定</target>
@@ -1172,19 +1143,11 @@
<target>接続</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve">
<source>Connect automatically</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>シークレットモードで接続</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve">
<source>Connect to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve">
<source>Connect to yourself?</source>
<note>No comment provided by engineer.</note>
@@ -1208,6 +1171,11 @@ This is your own one-time link!</source>
<target>リンク経由で接続</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>リンク・QRコード経由で接続</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>使い捨てリンク経由で接続しますか?</target>
@@ -1217,14 +1185,6 @@ This is your own one-time link!</source>
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source>
<target>サーバーに接続中…</target>
@@ -1235,10 +1195,6 @@ This is your own one-time link!</source>
<target>サーバーに接続中… (エラー: %@)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve">
<source>Connection</source>
<target>接続</target>
@@ -1259,10 +1215,6 @@ This is your own one-time link!</source>
<target>接続リクエストを送信しました!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>接続タイムアウト</target>
@@ -1342,10 +1294,6 @@ This is your own one-time link!</source>
<target>SimpleXアドレスの作成</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>人とつながるためのアドレスを作成する。</target>
@@ -1375,6 +1323,11 @@ This is your own one-time link!</source>
<target>[デスクトップアプリ](https://simplex.chat/downloads/)で新しいプロファイルを作成します。 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>使い捨ての招待リンクを生成する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1399,10 +1352,6 @@ This is your own one-time link!</source>
<target>%@ によって作成されました</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>現在のパスコード</target>
@@ -1749,18 +1698,6 @@ This cannot be undone!</source>
<target>説明</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve">
<source>Develop</source>
<target>開発</target>
@@ -1851,19 +1788,11 @@ This cannot be undone!</source>
<target>切断</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source>
<target>グループを見つけて参加する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve">
<source>Discover via local network</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
<source>Do NOT use SimpleX for emergency calls.</source>
<target>緊急通報にSimpleXを使用しないでください。</target>
@@ -1944,10 +1873,6 @@ This cannot be undone!</source>
<target>自動メッセージ削除を有効にしますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>すべて有効</target>
@@ -2013,10 +1938,6 @@ This cannot be undone!</source>
<target>暗号化されたメッセージまたは別のイベント</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>暗号化されたメッセージ : データベースエラー</target>
@@ -2042,14 +1963,6 @@ This cannot be undone!</source>
<target>暗号化されたメッセージ : 予期しないエラー</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source>
<target>パスコードを入力</target>
@@ -2079,10 +1992,6 @@ This cannot be undone!</source>
<target>サーバを手動で入力</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source>
<target>ウェルカムメッセージを入力してください…</target>
@@ -2241,10 +2150,6 @@ This cannot be undone!</source>
<target>%@ サーバーのロード中にエラーが発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>ファイル受信にエラー発生</target>
@@ -2285,10 +2190,6 @@ This cannot be undone!</source>
<target>ユーザーパスワード保存エラー</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>メールの送信にエラー発生</target>
@@ -2412,10 +2313,6 @@ This cannot be undone!</source>
<target>送信者がオンラインになるまでの待ち時間がなく、速い!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>お気に入り</target>
@@ -2511,10 +2408,6 @@ This cannot be undone!</source>
<target>コンソール</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve">
<source>French interface</source>
<target>フランス語UI</target>
@@ -2757,6 +2650,11 @@ This cannot be undone!</source>
<target>直接会えない場合は、ビデオ通話で QR コードを表示するか、リンクを共有してください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>直接会えない場合は、**ビデオ通話で QR コードを表示する**か、リンクを共有してください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>アプリを開くときにこのパスコードを入力すると、アプリのすべてのデータが元に戻せないように削除されます!</target>
@@ -2832,10 +2730,6 @@ This cannot be undone!</source>
<target>シークレットモード</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source>
<target>シークレットモード</target>
@@ -2866,10 +2760,6 @@ This cannot be undone!</source>
<target>データベースのバージョンと互換性がない</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source>
<target>パスコードが正しくありません</target>
@@ -2912,27 +2802,15 @@ This cannot be undone!</source>
<target>インターフェース</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>無効な接続リンク</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>無効なサーバアドレス!</target>
@@ -3047,18 +2925,6 @@ This is your link for group %@!</source>
<target>グループに参加</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>接続を維持</target>
@@ -3119,18 +2985,6 @@ This is your link for group %@!</source>
<target>制限事項</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source>
<target>ライブメッセージ!</target>
@@ -3141,11 +2995,6 @@ This is your link for group %@!</source>
<target>ライブメッセージ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>自分のみ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>ローカルネーム</target>
@@ -3384,10 +3233,6 @@ This is your link for group %@!</source>
<target>新しいパスコード</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>新しい繋がりのリクエスト</target>
@@ -3488,10 +3333,6 @@ This is your link for group %@!</source>
<target>送受信済みのファイルがありません</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>通知</target>
@@ -3511,15 +3352,16 @@ This is your link for group %@!</source>
- メンバーを無効にする (メッセージの送信不可)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>オフ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>オフ(自分のみ)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>OK</target>
@@ -3659,16 +3501,9 @@ This is your link for group %@!</source>
<target>プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>データベースを開いています…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3711,8 +3546,9 @@ This is your link for group %@!</source>
<target>パスワードを表示する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>貼り付け</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve">
@@ -3720,10 +3556,16 @@ This is your link for group %@!</source>
<target>画像の貼り付け</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>頂いたリンクを貼り付ける</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。</target>
<note>placeholder</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
<target>あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。</target>
@@ -3759,11 +3601,6 @@ This is your link for group %@!</source>
<target>あなたと連絡先の設定を確認してください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>グループの管理者に連絡してください。</target>
@@ -3967,10 +3804,6 @@ Error: %@</source>
<target>詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/readme.html#connect-to-friends)をご覧ください。</target>
@@ -4178,10 +4011,6 @@ Error: %@</source>
<target>データベース復元エラー</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>開示する</target>
@@ -4312,10 +4141,6 @@ Error: %@</source>
<target>QRコードを読み込む</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source>
<target>コードを読み込む</target>
@@ -4336,10 +4161,6 @@ Error: %@</source>
<target>検索</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>待ち行列セキュリティ確認</target>
@@ -4532,10 +4353,6 @@ Error: %@</source>
<target>サーバ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source>
<target>1日に設定</target>
@@ -4606,8 +4423,9 @@ Error: %@</source>
<target>リンクを送る</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>使い捨ての招待リンクを共有</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4730,15 +4548,16 @@ Error: %@</source>
<target>誰か</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>新しいチャットを開始する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>チャットを開始する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>移行の開始</target>
@@ -4863,14 +4682,6 @@ Error: %@</source>
<target>タップしてシークレットモードで参加</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>タップして新しいチャットを始める</target>
@@ -4933,10 +4744,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>データベースのパスフレーズ変更が完了してません。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>承認済の接続がキャンセルされます!</target>
@@ -5002,10 +4809,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>現在のチャットプロフィールの新しい接続のサーバ **%@**。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>テーマ</target>
@@ -5046,10 +4849,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<note>No comment provided by engineer.</note>
@@ -5082,10 +4881,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>接続するにはQRコードを読み込むか、アプリ内のリンクを使用する必要があります。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>新規に接続する場合</target>
@@ -5152,6 +4947,11 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>オフにする</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>通知をオフにしますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>オンにする</target>
@@ -5236,14 +5036,6 @@ To connect, please ask your contact to create another connection link and check
接続するには、連絡先に別の接続リンクを作成するよう依頼し、ネットワーク接続が安定していることを確認してください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source>
<target>ロック解除</target>
@@ -5334,10 +5126,6 @@ To connect, please ask your contact to create another connection link and check
<target>新しい接続に使う</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source>
<target>iOS通話インターフェースを使用する</target>
@@ -5348,10 +5136,6 @@ To connect, please ask your contact to create another connection link and check
<target>新しいシークレットプロファイルを使用する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>サーバを使う</target>
@@ -5372,23 +5156,11 @@ To connect, please ask your contact to create another connection link and check
<target>SimpleX チャット サーバーを使用する。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source>
<target>接続のセキュリティを確認</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source>
<target>セキュリティコードを確認</target>
@@ -5399,10 +5171,6 @@ To connect, please ask your contact to create another connection link and check
<target>ブラウザ経由</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve">
<source>Video call</source>
<target>ビデオ通話</target>
@@ -5453,10 +5221,6 @@ To connect, please ask your contact to create another connection link and check
<target>音声メッセージ…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source>
<target>ファイル待ち</target>
@@ -5601,6 +5365,11 @@ Repeat join request?</source>
<target>デバイスやアプリの認証を行わずに、ロック画面から通話を受けることができます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>リンクをクリックすることでも接続できます。ブラウザで開いた場合は、**モバイルアプリで開く**ボタンをクリックしてください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>後からでも作成できます</target>
@@ -5621,10 +5390,6 @@ Repeat join request?</source>
<target>ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>%@ にメッセージを送信できるようになりました</target>
@@ -5665,10 +5430,6 @@ Repeat join request?</source>
<target>メッセージの書式にmarkdownを使用することができます:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>メッセージを送信できませんでした!</target>
@@ -5853,6 +5614,13 @@ You can cancel this connection and remove the contact (and try later with a new
<target>連絡先がメッセージの完全削除を許可できます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>SimpleX の連絡先に表示されます。
設定で変更できます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>連絡先は接続されたままになります。</target>
@@ -5983,10 +5751,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>音声通話 (エンドツーエンド暗号化なし)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source>
<target>メッセージ ID が正しくありません</target>
@@ -6565,10 +6329,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>グループプロフィールを更新しました</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source>
<target>v%@ (%@)</target>
@@ -6706,10 +6466,6 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
<target>SimpleX はローカル認証に Face ID を使用します</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX では、音声通話やビデオ通話、および音声メッセージの録音のためにマイクへのアクセスが必要です。</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -284,30 +284,19 @@
<target>(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
<source>)</source>
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**สร้างลิงค์ / คิวอาร์โค้ด** เพื่อให้ผู้ติดต่อของคุณใช้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -320,6 +309,11 @@
<target>**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**แปะลิงก์ที่ได้รับ** หรือเปิดในเบราว์เซอร์แล้วแตะ **เปิดในแอปมือถือ**</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target>
@@ -330,6 +324,11 @@
<target>**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**สแกนคิวอาร์โค้ด**: เพื่อเชื่อมต่อกับผู้ติดต่อของคุณด้วยตนเองหรือผ่านการสนทนาทางวิดีโอ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain</target>
@@ -370,12 +369,6 @@
- และอื่น ๆ!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -424,6 +417,11 @@
<target>1 สัปดาห์</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>ลิงก์สำหรับใช้ 1 ครั้ง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5 นาที</target>
@@ -537,10 +535,6 @@
<target>เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target>
@@ -852,10 +846,6 @@
<target>กลับ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source>
<target>ID ข้อความที่ไม่ดี</target>
@@ -866,10 +856,6 @@
<target>แฮชข้อความไม่ดี</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>ข้อความที่ดีขึ้น</target>
@@ -879,10 +865,6 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
@@ -935,10 +917,6 @@
<target>โทร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!</target>
@@ -1055,10 +1033,6 @@
<target>การแชทหยุดทํางานแล้ว</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>ค่ากําหนดในการแชท</target>
@@ -1159,18 +1133,10 @@
<target>เชื่อมต่อ</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve">
<source>Connect automatically</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve">
<source>Connect to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve">
<source>Connect to yourself?</source>
<note>No comment provided by engineer.</note>
@@ -1194,6 +1160,11 @@ This is your own one-time link!</source>
<target>เชื่อมต่อผ่านลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>เชื่อมต่อผ่านลิงค์ / คิวอาร์โค้ด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<note>No comment provided by engineer.</note>
@@ -1202,14 +1173,6 @@ This is your own one-time link!</source>
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source>
<target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์…</target>
@@ -1220,10 +1183,6 @@ This is your own one-time link!</source>
<target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve">
<source>Connection</source>
<target>การเชื่อมต่อ</target>
@@ -1244,10 +1203,6 @@ This is your own one-time link!</source>
<target>ส่งคําขอเชื่อมต่อแล้ว!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>หมดเวลาการเชื่อมต่อ</target>
@@ -1327,10 +1282,6 @@ This is your own one-time link!</source>
<target>สร้างที่อยู่ SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ</target>
@@ -1359,6 +1310,11 @@ This is your own one-time link!</source>
<source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1383,10 +1339,6 @@ This is your own one-time link!</source>
<target>สร้างเมื่อ %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>รหัสผ่านปัจจุบัน</target>
@@ -1732,18 +1684,6 @@ This cannot be undone!</source>
<target>คำอธิบาย</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve">
<source>Develop</source>
<target>พัฒนา</target>
@@ -1834,18 +1774,10 @@ This cannot be undone!</source>
<target>ตัดการเชื่อมต่อ</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve">
<source>Discover via local network</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
<source>Do NOT use SimpleX for emergency calls.</source>
<target>อย่าใช้ SimpleX สําหรับการโทรฉุกเฉิน</target>
@@ -1926,10 +1858,6 @@ This cannot be undone!</source>
<target>เปิดใช้งานการลบข้อความอัตโนมัติ?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>เปิดใช้งานสําหรับทุกคน</target>
@@ -1993,10 +1921,6 @@ This cannot be undone!</source>
<target>ข้อความที่ encrypt หรือเหตุการณ์อื่น</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>ข้อความที่ encrypt: ความผิดพลาดในฐานข้อมูล</target>
@@ -2022,14 +1946,6 @@ This cannot be undone!</source>
<target>ข้อความที่ encrypt: ข้อผิดพลาดที่ไม่คาดคิด</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source>
<target>ใส่รหัสผ่าน</target>
@@ -2059,10 +1975,6 @@ This cannot be undone!</source>
<target>ใส่เซิร์ฟเวอร์ด้วยตนเอง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source>
<target>ใส่ข้อความต้อนรับ…</target>
@@ -2220,10 +2132,6 @@ This cannot be undone!</source>
<target>โหลดเซิร์ฟเวอร์ %@ ผิดพลาด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>เกิดข้อผิดพลาดในการรับไฟล์</target>
@@ -2264,10 +2172,6 @@ This cannot be undone!</source>
<target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านผู้ใช้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>เกิดข้อผิดพลาดในการส่งอีเมล</target>
@@ -2391,10 +2295,6 @@ This cannot be undone!</source>
<target>รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>ที่ชอบ</target>
@@ -2490,10 +2390,6 @@ This cannot be undone!</source>
<target>สำหรับคอนโซล</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve">
<source>French interface</source>
<target>อินเทอร์เฟซภาษาฝรั่งเศส</target>
@@ -2736,6 +2632,11 @@ This cannot be undone!</source>
<target>หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>หากคุณไม่สามารถพบปะด้วยตนเอง คุณสามารถ **สแกนคิวอาร์โค้ดผ่านการสนทนาทางวิดีโอ** หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!</target>
@@ -2811,10 +2712,6 @@ This cannot be undone!</source>
<target>ไม่ระบุตัวตน</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source>
<target>โหมดไม่ระบุตัวตน</target>
@@ -2844,10 +2741,6 @@ This cannot be undone!</source>
<target>เวอร์ชันฐานข้อมูลที่เข้ากันไม่ได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source>
<target>รหัสผ่านไม่ถูกต้อง</target>
@@ -2890,27 +2783,15 @@ This cannot be undone!</source>
<target>อินเตอร์เฟซ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>ลิงค์เชื่อมต่อไม่ถูกต้อง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</target>
@@ -3024,18 +2905,6 @@ This is your link for group %@!</source>
<target>กำลังจะเข้าร่วมกลุ่ม</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>รักษาการเชื่อมต่อของคุณ</target>
@@ -3096,18 +2965,6 @@ This is your link for group %@!</source>
<target>ข้อจำกัด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source>
<target>ข้อความสด!</target>
@@ -3118,11 +2975,6 @@ This is your link for group %@!</source>
<target>ข้อความสด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>ในเครื่อง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>ชื่อภายในเครื่องเท่านั้น</target>
@@ -3361,10 +3213,6 @@ This is your link for group %@!</source>
<target>รหัสผ่านใหม่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>คำขอติดต่อใหม่</target>
@@ -3463,10 +3311,6 @@ This is your link for group %@!</source>
<target>ไม่มีไฟล์ที่ได้รับหรือส่ง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>การแจ้งเตือน</target>
@@ -3486,15 +3330,16 @@ This is your link for group %@!</source>
- ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>ปิด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>ปิด (ในเครื่อง)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>ตกลง</target>
@@ -3633,16 +3478,9 @@ This is your link for group %@!</source>
<target>โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>กำลังเปิดฐานข้อมูล…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3685,8 +3523,9 @@ This is your link for group %@!</source>
<target>รหัสผ่านที่จะแสดง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>แปะ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve">
@@ -3694,10 +3533,15 @@ This is your link for group %@!</source>
<target>แปะภาพ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>แปะลิงก์ที่ได้รับ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<note>placeholder</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
<target>ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น</target>
@@ -3733,11 +3577,6 @@ This is your link for group %@!</source>
<target>โปรดตรวจสอบความต้องการของคุณและการตั้งค่าผู้ติดต่อ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>โปรดติดต่อผู้ดูแลกลุ่ม</target>
@@ -3941,10 +3780,6 @@ Error: %@</source>
<target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends)</target>
@@ -4151,10 +3986,6 @@ Error: %@</source>
<target>กู้คืนข้อผิดพลาดของฐานข้อมูล</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>เปิดเผย</target>
@@ -4285,10 +4116,6 @@ Error: %@</source>
<target>สแกนคิวอาร์โค้ด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source>
<target>สแกนรหัส</target>
@@ -4309,10 +4136,6 @@ Error: %@</source>
<target>ค้นหา</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>คิวที่ปลอดภัย</target>
@@ -4510,10 +4333,6 @@ Error: %@</source>
<target>เซิร์ฟเวอร์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source>
<target>ตั้ง 1 วัน</target>
@@ -4584,8 +4403,9 @@ Error: %@</source>
<target>แชร์ลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>แชร์ลิงก์เชิญแบบใช้ครั้งเดียว</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4705,15 +4525,16 @@ Error: %@</source>
<target>ใครบางคน</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>เริ่มแชทใหม่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>เริ่มแชท</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>เริ่มการย้ายข้อมูล</target>
@@ -4838,14 +4659,6 @@ Error: %@</source>
<target>แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>แตะเพื่อเริ่มแชทใหม่</target>
@@ -4909,10 +4722,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก!</target>
@@ -4978,10 +4787,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>ธีม</target>
@@ -5021,10 +4826,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<note>No comment provided by engineer.</note>
@@ -5057,10 +4858,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>เพื่อการเชื่อมต่อ ผู้ติดต่อของคุณสามารถสแกนคิวอาร์โค้ดหรือใช้ลิงก์ในแอป</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>เพื่อสร้างการเชื่อมต่อใหม่</target>
@@ -5127,6 +4924,11 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>ปิด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>ปิดการแจ้งเตือนไหม?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>เปิด</target>
@@ -5211,14 +5013,6 @@ To connect, please ask your contact to create another connection link and check
ในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source>
<target>ปลดล็อค</target>
@@ -5308,10 +5102,6 @@ To connect, please ask your contact to create another connection link and check
<target>ใช้สำหรับการเชื่อมต่อใหม่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source>
<target>ใช้อินเทอร์เฟซการโทร iOS</target>
@@ -5321,10 +5111,6 @@ To connect, please ask your contact to create another connection link and check
<source>Use new incognito profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>ใช้เซิร์ฟเวอร์</target>
@@ -5345,23 +5131,11 @@ To connect, please ask your contact to create another connection link and check
<target>กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source>
<target>ตรวจสอบความปลอดภัยในการเชื่อมต่อ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source>
<target>ตรวจสอบรหัสความปลอดภัย</target>
@@ -5372,10 +5146,6 @@ To connect, please ask your contact to create another connection link and check
<target>ผ่านเบราว์เซอร์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve">
<source>Video call</source>
<target>การสนทนาทางวิดีโอ</target>
@@ -5426,10 +5196,6 @@ To connect, please ask your contact to create another connection link and check
<target>ข้อความเสียง…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source>
<target>กำลังรอไฟล์</target>
@@ -5574,6 +5340,11 @@ Repeat join request?</source>
<target>คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม **เปิดในแอปมือถือ**</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>คุณสามารถสร้างได้ในภายหลัง</target>
@@ -5594,10 +5365,6 @@ Repeat join request?</source>
<target>คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>ตอนนี้คุณสามารถส่งข้อความถึง %@</target>
@@ -5638,10 +5405,6 @@ Repeat join request?</source>
<target>คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>คุณไม่สามารถส่งข้อความได้!</target>
@@ -5825,6 +5588,13 @@ You can cancel this connection and remove the contact (and try later with a new
<target>ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>ผู้ติดต่อของคุณใน SimpleX จะเห็น
คุณสามารถเปลี่ยนได้ในการตั้งค่า</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่</target>
@@ -5954,10 +5724,6 @@ SimpleX servers cannot see your profile.</source>
<target>การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source>
<target>ID ข้อความที่ไม่ดี</target>
@@ -6534,10 +6300,6 @@ SimpleX servers cannot see your profile.</source>
<target>อัปเดตโปรไฟล์กลุ่มแล้ว</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source>
<target>v%@ (%@)</target>
@@ -6675,10 +6437,6 @@ SimpleX servers cannot see your profile.</source>
<target>SimpleX ใช้ Face ID สำหรับการรับรองความถูกต้องในเครื่อง</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX ต้องการการเข้าถึงไมโครโฟนสำหรับการโทรด้วยเสียงและวิดีโอ และเพื่อบันทึกข้อความเสียง</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -5,31 +5,25 @@
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/>
</header>
<body>
<trans-unit id="&#10;" xml:space="preserve" approved="no">
<trans-unit id="&#10;" xml:space="preserve">
<source>
</source>
<target state="needs-translation">
</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" " xml:space="preserve" approved="no">
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target state="needs-translation"> </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" " xml:space="preserve" approved="no">
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target state="needs-translation"> </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" " xml:space="preserve" approved="no">
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target state="translated"> </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" (" xml:space="preserve" approved="no">
<trans-unit id=" (" xml:space="preserve">
<source> (</source>
<target state="needs-translation"> (</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" (can be copied)" xml:space="preserve" approved="no">
@@ -349,9 +343,8 @@
<target state="translated">30 saniye</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=": " xml:space="preserve" approved="no">
<trans-unit id=": " xml:space="preserve">
<source>: </source>
<target state="needs-translation">: </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="&lt;p&gt;Hi!&lt;/p&gt;&#10;&lt;p&gt;&lt;a href=&quot;%@&quot;&gt;Connect to me via SimpleX Chat&lt;/a&gt;&lt;/p&gt;" xml:space="preserve" approved="no">
@@ -397,9 +390,8 @@
<source>Abort changing address?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX" xml:space="preserve" approved="no">
<trans-unit id="About SimpleX" xml:space="preserve">
<source>About SimpleX</source>
<target state="translated">SimpleX Hakkında</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="About SimpleX Chat" xml:space="preserve">
@@ -414,9 +406,8 @@
<source>Accent color</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accept" xml:space="preserve" approved="no">
<trans-unit id="Accept" xml:space="preserve">
<source>Accept</source>
<target state="translated">Kabul et</target>
<note>accept contact request via notification
accept incoming call via notification</note>
</trans-unit>
@@ -444,57 +435,48 @@
<source>Add profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve" approved="no">
<trans-unit id="Add servers by scanning QR codes." xml:space="preserve">
<source>Add servers by scanning QR codes.</source>
<target state="translated">Karekod taratarak sunucuları ekleyin.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add server…" xml:space="preserve" approved="no">
<trans-unit id="Add server…" xml:space="preserve">
<source>Add server…</source>
<target state="translated">Sunucu ekle…</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add to another device" xml:space="preserve">
<source>Add to another device</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add welcome message" xml:space="preserve" approved="no">
<trans-unit id="Add welcome message" xml:space="preserve">
<source>Add welcome message</source>
<target state="translated">Karşılama mesajı ekleyin</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Address" xml:space="preserve" approved="no">
<trans-unit id="Address" xml:space="preserve">
<source>Address</source>
<target state="translated">Adres</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no">
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target state="translated">Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
<source>Admins can create the links to join groups.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Advanced network settings" xml:space="preserve" approved="no">
<trans-unit id="Advanced network settings" xml:space="preserve">
<source>Advanced network settings</source>
<target state="translated">GGelişmiş ağ ayarları</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All app data is deleted." xml:space="preserve" approved="no">
<trans-unit id="All app data is deleted." xml:space="preserve">
<source>All app data is deleted.</source>
<target state="translated">Tüm uygulama verileri silinir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no">
<trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All chats and messages will be deleted - this cannot be undone!</source>
<target state="translated">Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is erased when it is entered." xml:space="preserve" approved="no">
<trans-unit id="All data is erased when it is entered." xml:space="preserve">
<source>All data is erased when it is entered.</source>
<target state="translated">Kullanıldığında bütün veriler silinir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve" approved="no">
@@ -502,29 +484,24 @@
<target state="translated">Tüm grup üyeleri bağlı kalacaktır.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve" approved="no">
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target state="translated">Tüm mesajlar silinecektir. Bu, geri alınamaz! Mesajlar, YALNIZCA senin için silinecektir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve" approved="no">
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
<target state="translated">Konuştuğun kişilerin tümü bağlı kalacaktır.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve" approved="no">
<trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve">
<source>All your contacts will remain connected. Profile update will be sent to your contacts.</source>
<target state="translated">Tüm kişileriniz bağlı kalacaktır. Profil güncellemesi kişilerinize gönderilecektir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow" xml:space="preserve" approved="no">
<trans-unit id="Allow" xml:space="preserve">
<source>Allow</source>
<target state="translated">İzin ver</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow calls only if your contact allows them." xml:space="preserve" approved="no">
<trans-unit id="Allow calls only if your contact allows them." xml:space="preserve">
<source>Allow calls only if your contact allows them.</source>
<target state="translated">Yalnızca irtibat kişiniz izin veriyorsa aramalara izin verin.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve">
@@ -535,9 +512,8 @@
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve" approved="no">
<trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve">
<source>Allow message reactions only if your contact allows them.</source>
<target state="translated">Yalnızca kişin mesaj tepkilerine izin veriyorsa sen de ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow message reactions." xml:space="preserve">
@@ -549,39 +525,32 @@
<target state="translated">Üyelere direkt mesaj göndermeye izin ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow sending disappearing messages." xml:space="preserve" approved="no">
<trans-unit id="Allow sending disappearing messages." xml:space="preserve">
<source>Allow sending disappearing messages.</source>
<target state="translated">Kendiliğinden yok olan mesajlar göndermeye izin ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve" approved="no">
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
<source>Allow to irreversibly delete sent messages.</source>
<target state="translated">Gönderilen mesajların kalıcı olarak silinmesine izin ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve" approved="no">
<trans-unit id="Allow to send files and media." xml:space="preserve">
<source>Allow to send files and media.</source>
<target state="translated">Dosya ve medya göndermeye izin ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send voice messages." xml:space="preserve" approved="no">
<trans-unit id="Allow to send voice messages." xml:space="preserve">
<source>Allow to send voice messages.</source>
<target state="translated">Sesli mesaj göndermeye izin ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve" approved="no">
<trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve">
<source>Allow voice messages only if your contact allows them.</source>
<target state="translated">Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow voice messages?" xml:space="preserve" approved="no">
<trans-unit id="Allow voice messages?" xml:space="preserve">
<source>Allow voice messages?</source>
<target state="translated">Sesli mesajlara izin ver?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts adding message reactions." xml:space="preserve" approved="no">
<trans-unit id="Allow your contacts adding message reactions." xml:space="preserve">
<source>Allow your contacts adding message reactions.</source>
<target state="translated">Konuştuğun kişilerin mesajlarına tepki eklemesine izin ver.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to call you." xml:space="preserve">
@@ -608,28 +577,24 @@
<source>Always use relay</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve" approved="no">
<trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve">
<source>An empty chat profile with the provided name is created, and the app opens as usual.</source>
<target state="translated">Verilen adla boş bir sohbet profili oluşturulur ve uygulama her zamanki gibi açılır.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Answer call" xml:space="preserve" approved="no">
<trans-unit id="Answer call" xml:space="preserve">
<source>Answer call</source>
<target state="translated">Aramayı cevapla</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App build: %@" xml:space="preserve">
<source>App build: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App icon" xml:space="preserve" approved="no">
<trans-unit id="App icon" xml:space="preserve">
<source>App icon</source>
<target state="translated">Uygulama simgesi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App passcode" xml:space="preserve" approved="no">
<trans-unit id="App passcode" xml:space="preserve">
<source>App passcode</source>
<target state="translated">Uygulama erişim kodu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve">
@@ -4928,41 +4893,6 @@ SimpleX servers cannot see your profile.</source>
<target state="needs-translation">\~strike~</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accept connection request?" xml:space="preserve" approved="no">
<source>Accept connection request?</source>
<target state="translated">Bağlantı isteğini kabul et?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="needs-translation"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="Already connecting!" xml:space="preserve" approved="no">
<source>Already connecting!</source>
<target state="translated">Zaten bağlanılıyor!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve" approved="no">
<source>A few more things</source>
<target state="translated">Birkaç şey daha</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve" approved="no">
<source>## History</source>
<target state="translated">## Geçmiş</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no">
<source>A new random profile will be shared.</source>
<target state="translated">Yeni bir rastgele profil paylaşılacak.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Already joining the group!" xml:space="preserve" approved="no">
<source>Already joining the group!</source>
<target state="translated">Zaten gruba bağlanılıyor!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="tr" datatype="plaintext">
File diff suppressed because it is too large Load Diff
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -290,30 +290,19 @@
<target>(</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve">
<source>(new)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve">
<source>(this device v%@)</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=")" xml:space="preserve">
<source>)</source>
<target>)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
<target>**添加新联系人**:为您的联系人创建一次性二维码或者链接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve">
<source>**Create group**: to create a new group.</source>
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
<source>**Create link / QR code** for your contact to use.</source>
<target>**创建链接 / 二维码** 给您的联系人使用。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
@@ -326,6 +315,11 @@
<target>**最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
<target>**粘贴收到的链接**或者在浏览器里打开并且点击**在移动应用程序里打开**。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
<target>**请注意**:如果您丢失密码,您将无法恢复或者更改密码。</target>
@@ -336,6 +330,11 @@
<target>**推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
<source>**Scan QR code**: to connect to your contact in person or via video call.</source>
<target>**扫描二维码**:见面或者通过视频通话来连接您的联系人。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
<target>**警告**:及时推送通知需要保存在钥匙串的密码。</target>
@@ -379,12 +378,6 @@
- 以及更多!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="- voice messages up to 5 minutes.&#10;- custom time to disappear.&#10;- editing history." xml:space="preserve">
<source>- voice messages up to 5 minutes.
- custom time to disappear.
@@ -433,6 +426,11 @@
<target>1周</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="1-time link" xml:space="preserve">
<source>1-time link</source>
<target>一次性链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="5 minutes" xml:space="preserve">
<source>5 minutes</source>
<target>5分钟</target>
@@ -548,10 +546,6 @@
<target>将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve">
<source>Add contact</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Add preset servers" xml:space="preserve">
<source>Add preset servers</source>
<target>添加预设服务器</target>
@@ -864,10 +858,6 @@
<target>返回</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve">
<source>Bad desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Bad message ID" xml:space="preserve">
<source>Bad message ID</source>
<target>错误消息 ID</target>
@@ -878,10 +868,6 @@
<target>错误消息散列</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>更好的消息</target>
@@ -891,10 +877,6 @@
<source>Block</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
<source>Block group members</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve">
<source>Block member</source>
<note>No comment provided by engineer.</note>
@@ -948,10 +930,6 @@
<target>通话</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve">
<source>Camera not available</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete user profile!" xml:space="preserve">
<source>Can't delete user profile!</source>
<target>无法删除用户个人资料!</target>
@@ -1068,10 +1046,6 @@
<target>聊天已停止</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." xml:space="preserve">
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences" xml:space="preserve">
<source>Chat preferences</source>
<target>聊天偏好设置</target>
@@ -1172,19 +1146,11 @@
<target>连接</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Connect automatically" xml:space="preserve">
<source>Connect automatically</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect incognito" xml:space="preserve">
<source>Connect incognito</source>
<target>在隐身状态下连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to desktop" xml:space="preserve">
<source>Connect to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect to yourself?" xml:space="preserve">
<source>Connect to yourself?</source>
<note>No comment provided by engineer.</note>
@@ -1208,6 +1174,11 @@ This is your own one-time link!</source>
<target>通过链接连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link / QR code" xml:space="preserve">
<source>Connect via link / QR code</source>
<target>通过群组链接/二维码连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via one-time link" xml:space="preserve">
<source>Connect via one-time link</source>
<target>通过一次性链接连接</target>
@@ -1217,14 +1188,6 @@ This is your own one-time link!</source>
<source>Connect with %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected desktop" xml:space="preserve">
<source>Connected desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connected to desktop" xml:space="preserve">
<source>Connected to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting server…" xml:space="preserve">
<source>Connecting to server…</source>
<target>连接服务器中……</target>
@@ -1235,10 +1198,6 @@ This is your own one-time link!</source>
<target>连接服务器中……(错误:%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connecting to desktop" xml:space="preserve">
<source>Connecting to desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection" xml:space="preserve">
<source>Connection</source>
<target>连接</target>
@@ -1259,10 +1218,6 @@ This is your own one-time link!</source>
<target>已发送连接请求!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection terminated" xml:space="preserve">
<source>Connection terminated</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source>
<target>连接超时</target>
@@ -1342,10 +1297,6 @@ This is your own one-time link!</source>
<target>创建 SimpleX 地址</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create a group using a random profile." xml:space="preserve">
<source>Create a group using a random profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
<source>Create an address to let people connect with you.</source>
<target>创建一个地址,让人们与您联系。</target>
@@ -1375,6 +1326,11 @@ This is your own one-time link!</source>
<target>在[桌面应用程序](https://simplex.chat/downloads/)中创建新的个人资料。 💻</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>创建一次性邀请链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create profile" xml:space="preserve">
<source>Create profile</source>
<note>No comment provided by engineer.</note>
@@ -1399,10 +1355,6 @@ This is your own one-time link!</source>
<target>创建于 %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Creating link…" xml:space="preserve">
<source>Creating link…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Current Passcode" xml:space="preserve">
<source>Current Passcode</source>
<target>当前密码</target>
@@ -1749,18 +1701,6 @@ This cannot be undone!</source>
<target>描述</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop address" xml:space="preserve">
<source>Desktop address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop app version %@ is not compatible with this app." xml:space="preserve">
<source>Desktop app version %@ is not compatible with this app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Desktop devices" xml:space="preserve">
<source>Desktop devices</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Develop" xml:space="preserve">
<source>Develop</source>
<target>开发</target>
@@ -1851,19 +1791,11 @@ This cannot be undone!</source>
<target>断开连接</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
<source>Discover and join groups</source>
<target>发现和加入群组</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover via local network" xml:space="preserve">
<source>Discover via local network</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve">
<source>Do NOT use SimpleX for emergency calls.</source>
<target>请勿使用 SimpleX 进行紧急通话。</target>
@@ -1944,10 +1876,6 @@ This cannot be undone!</source>
<target>启用自动删除消息?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable camera access" xml:space="preserve">
<source>Enable camera access</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enable for all" xml:space="preserve">
<source>Enable for all</source>
<target>全部启用</target>
@@ -2013,10 +1941,6 @@ This cannot be undone!</source>
<target>加密消息或其他事件</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: app is stopped" xml:space="preserve">
<source>Encrypted message: app is stopped</source>
<note>notification</note>
</trans-unit>
<trans-unit id="Encrypted message: database error" xml:space="preserve">
<source>Encrypted message: database error</source>
<target>加密消息:数据库错误</target>
@@ -2042,14 +1966,6 @@ This cannot be undone!</source>
<target>加密消息:意外错误</target>
<note>notification</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation error" xml:space="preserve">
<source>Encryption re-negotiation error</source>
<note>message decrypt error item</note>
</trans-unit>
<trans-unit id="Encryption re-negotiation failed." xml:space="preserve">
<source>Encryption re-negotiation failed.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter Passcode" xml:space="preserve">
<source>Enter Passcode</source>
<target>输入密码</target>
@@ -2079,10 +1995,6 @@ This cannot be undone!</source>
<target>手动输入服务器</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter this device name…" xml:space="preserve">
<source>Enter this device name…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Enter welcome message…" xml:space="preserve">
<source>Enter welcome message…</source>
<target>输入欢迎消息……</target>
@@ -2242,10 +2154,6 @@ This cannot be undone!</source>
<target>加载 %@ 服务器错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>接收文件错误</target>
@@ -2286,10 +2194,6 @@ This cannot be undone!</source>
<target>保存用户密码时出错</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve">
<source>Error scanning code: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error sending email" xml:space="preserve">
<source>Error sending email</source>
<target>发送电邮错误</target>
@@ -2414,10 +2318,6 @@ This cannot be undone!</source>
<target>快速且无需等待发件人在线!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Faster joining and more reliable messages." xml:space="preserve">
<source>Faster joining and more reliable messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Favorite" xml:space="preserve">
<source>Favorite</source>
<target>最喜欢</target>
@@ -2513,10 +2413,6 @@ This cannot be undone!</source>
<target>用于控制台</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Found desktop" xml:space="preserve">
<source>Found desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="French interface" xml:space="preserve">
<source>French interface</source>
<target>法语界面</target>
@@ -2759,6 +2655,11 @@ This cannot be undone!</source>
<target>如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
<source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source>
<target>如果您不能亲自见面,您可以**扫描视频通话中的二维码**,或者您的联系人可以分享邀请链接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
<target>如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!</target>
@@ -2834,10 +2735,6 @@ This cannot be undone!</source>
<target>隐身聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito groups" xml:space="preserve">
<source>Incognito groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incognito mode" xml:space="preserve">
<source>Incognito mode</source>
<target>隐身模式</target>
@@ -2868,10 +2765,6 @@ This cannot be undone!</source>
<target>数据库版本不兼容</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incompatible version" xml:space="preserve">
<source>Incompatible version</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Incorrect passcode" xml:space="preserve">
<source>Incorrect passcode</source>
<target>密码错误</target>
@@ -2914,27 +2807,15 @@ This cannot be undone!</source>
<target>界面</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid QR code" xml:space="preserve">
<source>Invalid QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid connection link" xml:space="preserve">
<source>Invalid connection link</source>
<target>无效的连接链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid name!" xml:space="preserve">
<source>Invalid name!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid response" xml:space="preserve">
<source>Invalid response</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid server address!" xml:space="preserve">
<source>Invalid server address!</source>
<target>无效的服务器地址!</target>
@@ -3049,18 +2930,6 @@ This is your link for group %@!</source>
<target>加入群组中</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep" xml:space="preserve">
<source>Keep</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
<source>Keep the app open to use it from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep unused invitation?" xml:space="preserve">
<source>Keep unused invitation?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep your connections" xml:space="preserve">
<source>Keep your connections</source>
<target>保持连接</target>
@@ -3121,18 +2990,6 @@ This is your link for group %@!</source>
<target>限制</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Link mobile and desktop apps! 🔗" xml:space="preserve">
<source>Link mobile and desktop apps! 🔗</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktop options" xml:space="preserve">
<source>Linked desktop options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Linked desktops" xml:space="preserve">
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Live message!" xml:space="preserve">
<source>Live message!</source>
<target>实时消息!</target>
@@ -3143,11 +3000,6 @@ This is your link for group %@!</source>
<target>实时消息</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>本地</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Local name" xml:space="preserve">
<source>Local name</source>
<target>本地名称</target>
@@ -3387,10 +3239,6 @@ This is your link for group %@!</source>
<target>新密码</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New contact request" xml:space="preserve">
<source>New contact request</source>
<target>新联系人请求</target>
@@ -3491,10 +3339,6 @@ This is your link for group %@!</source>
<target>未收到或发送文件</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Not compatible!" xml:space="preserve">
<source>Not compatible!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>通知</target>
@@ -3514,15 +3358,16 @@ This is your link for group %@!</source>
- 禁用成员(“观察员”角色)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="OK" xml:space="preserve">
<source>OK</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off" xml:space="preserve">
<source>Off</source>
<target>关闭</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Off (Local)" xml:space="preserve">
<source>Off (Local)</source>
<target>关闭(本地)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>好的</target>
@@ -3662,16 +3507,9 @@ This is your link for group %@!</source>
<target>开源协议和代码——任何人都可以运行服务器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Opening app…" xml:space="preserve">
<source>Opening app…</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or scan QR code" xml:space="preserve">
<source>Or scan QR code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Or show this code" xml:space="preserve">
<source>Or show this code</source>
<trans-unit id="Opening database…" xml:space="preserve">
<source>Opening database…</source>
<target>打开数据库中……</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
@@ -3714,8 +3552,9 @@ This is your link for group %@!</source>
<target>显示密码</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
<source>Paste desktop address</source>
<trans-unit id="Paste" xml:space="preserve">
<source>Paste</source>
<target>粘贴</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste image" xml:space="preserve">
@@ -3723,10 +3562,16 @@ This is your link for group %@!</source>
<target>粘贴图片</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
<source>Paste the link you received</source>
<trans-unit id="Paste received link" xml:space="preserve">
<source>Paste received link</source>
<target>粘贴收到的链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve">
<source>Paste the link you received to connect with your contact.</source>
<target>将您收到的链接粘贴到下面的框中以与您的联系人联系。</target>
<note>placeholder</note>
</trans-unit>
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
<source>People can connect to you only via the links you share.</source>
<target>人们只能通过您共享的链接与您建立联系。</target>
@@ -3762,11 +3607,6 @@ This is your link for group %@!</source>
<target>请检查您和您的联系人偏好设置。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact developers.&#10;Error: %@" xml:space="preserve">
<source>Please contact developers.
Error: %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please contact group admin." xml:space="preserve">
<source>Please contact group admin.</source>
<target>请联系群组管理员。</target>
@@ -3970,10 +3810,6 @@ Error: %@</source>
<target>在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target>在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。</target>
@@ -4182,10 +4018,6 @@ Error: %@</source>
<target>恢复数据库错误</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Reveal" xml:space="preserve">
<source>Reveal</source>
<target>揭示</target>
@@ -4316,10 +4148,6 @@ Error: %@</source>
<target>扫描二维码</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan QR code from desktop" xml:space="preserve">
<source>Scan QR code from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scan code" xml:space="preserve">
<source>Scan code</source>
<target>扫码</target>
@@ -4340,10 +4168,6 @@ Error: %@</source>
<target>搜索</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
<source>Secure queue</source>
<target>保护队列</target>
@@ -4544,10 +4368,6 @@ Error: %@</source>
<target>服务器</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Session code" xml:space="preserve">
<source>Session code</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Set 1 day" xml:space="preserve">
<source>Set 1 day</source>
<target>设定1天</target>
@@ -4618,8 +4438,9 @@ Error: %@</source>
<target>分享链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<trans-unit id="Share one-time invitation link" xml:space="preserve">
<source>Share one-time invitation link</source>
<target>分享一次性邀请链接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share with contacts" xml:space="preserve">
@@ -4742,15 +4563,16 @@ Error: %@</source>
<target>某人</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="Start a new chat" xml:space="preserve">
<source>Start a new chat</source>
<target>开始新聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat" xml:space="preserve">
<source>Start chat</source>
<target>开始聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start chat?" xml:space="preserve">
<source>Start chat?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Start migration" xml:space="preserve">
<source>Start migration</source>
<target>开始迁移</target>
@@ -4875,14 +4697,6 @@ Error: %@</source>
<target>点击以加入隐身聊天</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve">
<source>Tap to paste link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tap to start a new chat" xml:space="preserve">
<source>Tap to start a new chat</source>
<target>点击开始一个新聊天</target>
@@ -4945,10 +4759,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>更改数据库密码的尝试未完成。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The code you scanned is not a SimpleX link QR code." xml:space="preserve">
<source>The code you scanned is not a SimpleX link QR code.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve">
<source>The connection you accepted will be cancelled!</source>
<target>您接受的连接将被取消!</target>
@@ -5014,10 +4824,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>您当前聊天资料 **%@** 的新连接服务器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The text you pasted is not a SimpleX link." xml:space="preserve">
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Theme" xml:space="preserve">
<source>Theme</source>
<target>主题</target>
@@ -5058,10 +4864,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This device name" xml:space="preserve">
<source>This device name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
<source>This group has over %lld members, delivery receipts are not sent.</source>
<target>该组有超过 %lld 个成员,不发送送货单。</target>
@@ -5095,10 +4897,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target>您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To hide unwanted messages." xml:space="preserve">
<source>To hide unwanted messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To make a new connection" xml:space="preserve">
<source>To make a new connection</source>
<target>建立新连接</target>
@@ -5166,6 +4964,11 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>关闭</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off notifications?" xml:space="preserve">
<source>Turn off notifications?</source>
<target>关闭通知?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn on" xml:space="preserve">
<source>Turn on</source>
<target>打开</target>
@@ -5250,14 +5053,6 @@ To connect, please ask your contact to create another connection link and check
如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink" xml:space="preserve">
<source>Unlink</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlink desktop?" xml:space="preserve">
<source>Unlink desktop?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unlock" xml:space="preserve">
<source>Unlock</source>
<target>解锁</target>
@@ -5348,10 +5143,6 @@ To connect, please ask your contact to create another connection link and check
<target>用于新连接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use from desktop" xml:space="preserve">
<source>Use from desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use iOS call interface" xml:space="preserve">
<source>Use iOS call interface</source>
<target>使用 iOS 通话界面</target>
@@ -5362,10 +5153,6 @@ To connect, please ask your contact to create another connection link and check
<target>使用新的隐身配置文件</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use only local notifications?" xml:space="preserve">
<source>Use only local notifications?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use server" xml:space="preserve">
<source>Use server</source>
<target>使用服务器</target>
@@ -5386,23 +5173,11 @@ To connect, please ask your contact to create another connection link and check
<target>使用 SimpleX Chat 服务器。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify code with desktop" xml:space="preserve">
<source>Verify code with desktop</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection" xml:space="preserve">
<source>Verify connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connection security" xml:space="preserve">
<source>Verify connection security</source>
<target>验证连接安全</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify connections" xml:space="preserve">
<source>Verify connections</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Verify security code" xml:space="preserve">
<source>Verify security code</source>
<target>验证安全码</target>
@@ -5413,10 +5188,6 @@ To connect, please ask your contact to create another connection link and check
<target>通过浏览器</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Via secure quantum resistant protocol." xml:space="preserve">
<source>Via secure quantum resistant protocol.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Video call" xml:space="preserve">
<source>Video call</source>
<target>视频通话</target>
@@ -5467,10 +5238,6 @@ To connect, please ask your contact to create another connection link and check
<target>语音消息……</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for desktop..." xml:space="preserve">
<source>Waiting for desktop...</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Waiting for file" xml:space="preserve">
<source>Waiting for file</source>
<target>等待文件中</target>
@@ -5615,6 +5382,11 @@ Repeat join request?</source>
<target>您可以从锁屏上接听电话,无需设备和应用程序的认证。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve">
<source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source>
<target>您也可以通过点击链接进行连接。如果在浏览器中打开,请点击“在移动应用程序中打开”按钮。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
<source>You can create it later</source>
<target>您可以以后创建它</target>
@@ -5635,10 +5407,6 @@ Repeat join request?</source>
<target>您可以隐藏或静音用户个人资料——只需向右滑动。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
<source>You can make it visible to your SimpleX contacts via Settings.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can now send messages to %@" xml:space="preserve">
<source>You can now send messages to %@</source>
<target>您现在可以给 %@ 发送消息</target>
@@ -5679,10 +5447,6 @@ Repeat join request?</source>
<target>您可以使用 markdown 来编排消息格式:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can view invitation link again in connection details." xml:space="preserve">
<source>You can view invitation link again in connection details.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can't send messages!" xml:space="preserve">
<source>You can't send messages!</source>
<target>您无法发送消息!</target>
@@ -5867,6 +5631,13 @@ You can cancel this connection and remove the contact (and try later with a new
<target>您的联系人可以允许完全删除消息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts in SimpleX will see it.&#10;You can change it in Settings." xml:space="preserve">
<source>Your contacts in SimpleX will see it.
You can change it in Settings.</source>
<target>您的 SimpleX 的联系人会看到它。
您可以在设置中更改它。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
<source>Your contacts will remain connected.</source>
<target>与您的联系人保持连接。</target>
@@ -5997,10 +5768,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>语音通话(非端到端加密)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="author" xml:space="preserve">
<source>author</source>
<note>member role</note>
</trans-unit>
<trans-unit id="bad message ID" xml:space="preserve">
<source>bad message ID</source>
<target>错误消息 ID</target>
@@ -6581,10 +6348,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>已更新的群组资料</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
<source>v%@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="v%@ (%@)" xml:space="preserve">
<source>v%@ (%@)</source>
<target>v%@ (%@)</target>
@@ -6722,10 +6485,6 @@ SimpleX 服务器无法看到您的资料。</target>
<target>SimpleX 使用Face ID进行本地身份验证</target>
<note>Privacy - Face ID Usage Description</note>
</trans-unit>
<trans-unit id="NSLocalNetworkUsageDescription" xml:space="preserve">
<source>SimpleX uses local network access to allow using user chat profile via desktop app on the same network.</source>
<note>Privacy - Local Network Usage Description</note>
</trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>SimpleX 需要麦克风访问权限才能进行音频和视频通话,以及录制语音消息。</target>
@@ -4,8 +4,6 @@
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Local Network Usage Description */
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
@@ -5868,36 +5868,6 @@ It can happen because of some bug or when the connection is compromised.</source
<target state="translated">你和你的聯絡人可以新增訊息互動。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ connected" xml:space="preserve" approved="no">
<source>%@ connected</source>
<target state="translated">%@ 已連接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="# %@" xml:space="preserve" approved="no">
<source># %@</source>
<target state="translated"># %@</target>
<note>copied message info title, # &lt;title&gt;</note>
</trans-unit>
<trans-unit id="%@ and %@" xml:space="preserve" approved="no">
<source>%@ and %@</source>
<target state="translated">%@ 和 %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="## History" xml:space="preserve" approved="no">
<source>## History</source>
<target state="translated">紀錄</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
<source>## In reply to</source>
<target state="translated">回覆</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
<source>%@ and %@ connected</source>
<target state="translated">%@ 和 %@ 已連接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hant" datatype="plaintext">
@@ -1,64 +0,0 @@
//
// ConcurrentQueue.swift
// SimpleX NSE
//
// Created by Evgeny on 08/12/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import Foundation
struct DequeueElement<T> {
var elementId: UUID?
var task: Task<T?, Never>
}
class ConcurrentQueue<T> {
private var queue: [T] = []
private var queueLock = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.concurrent-queue.lock.\(UUID())")
private var continuations = [(elementId: UUID, continuation: CheckedContinuation<T?, Never>)]()
func enqueue(_ el: T) {
resumeContinuation(el) { self.queue.append(el) }
}
func frontEnqueue(_ el: T) {
resumeContinuation(el) { self.queue.insert(el, at: 0) }
}
private func resumeContinuation(_ el: T, add: @escaping () -> Void) {
queueLock.sync {
if let (_, cont) = continuations.first {
continuations.remove(at: 0)
cont.resume(returning: el)
} else {
add()
}
}
}
func dequeue() -> DequeueElement<T> {
queueLock.sync {
if queue.isEmpty {
let elementId = UUID()
let task = Task {
await withCheckedContinuation { cont in
continuations.append((elementId, cont))
}
}
return DequeueElement(elementId: elementId, task: task)
} else {
let el = queue.remove(at: 0)
return DequeueElement(task: Task { el })
}
}
}
func cancelDequeue(_ elementId: UUID) {
queueLock.sync {
let cancelled = continuations.filter { $0.elementId == elementId }
continuations.removeAll { $0.elementId == elementId }
cancelled.forEach { $0.continuation.resume(returning: nil) }
}
}
}
+86 -420
View File
@@ -14,231 +14,91 @@ import SimpleXChat
let logger = Logger()
let appSuspendingDelay: UInt64 = 2_500_000_000
let suspendingDelay: UInt64 = 2_000_000_000
let nseSuspendDelay: TimeInterval = 2
typealias NtfStream = AsyncStream<NSENotification>
let nseSuspendTimeout: Int = 5
typealias NtfStream = ConcurrentQueue<NSENotification>
// Notifications are delivered via concurrent queues, as they are all received from chat controller in a single loop that
// writes to ConcurrentQueue and when notification is processed, the instance of Notification service extension reads from the queue.
// One queue per connection (entity) is used.
// The concurrent queues allow read cancellation, to ensure that notifications are not lost in case the current thread completes
// before expected notification is read (multiple notifications can be expected, because one notification can be delivered for several messages).
actor PendingNtfs {
static let shared = PendingNtfs()
private var ntfStreams: [String: NtfStream] = [:]
private var ntfConts: [String: NtfStream.Continuation] = [:]
func createStream(_ id: String) async {
logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)")
if ntfStreams[id] == nil {
ntfStreams[id] = ConcurrentQueue()
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
func createStream(_ id: String) {
logger.debug("PendingNtfs.createStream: \(id, privacy: .public)")
if ntfStreams.index(forKey: id) == nil {
ntfStreams[id] = AsyncStream { cont in
ntfConts[id] = cont
logger.debug("PendingNtfs.createStream: store continuation")
}
}
}
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async {
logger.debug("NotificationService PendingNtfs.readStream: \(id, privacy: .public) \(ntfInfo.ntfMessages.count, privacy: .public)")
if !ntfInfo.user.showNotifications {
nse.setBestAttemptNtf(.empty)
}
func readStream(_ id: String, for nse: NotificationService, msgCount: Int = 1, showNotifications: Bool) async {
logger.debug("PendingNtfs.readStream: \(id, privacy: .public) \(msgCount, privacy: .public)")
if let s = ntfStreams[id] {
logger.debug("NotificationService PendingNtfs.readStream: has stream")
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected, privacy: .public)")
var readCancelled = false
var dequeued: DequeueElement<NSENotification>?
nse.cancelRead = {
readCancelled = true
if let elementId = dequeued?.elementId {
s.cancelDequeue(elementId)
}
logger.debug("PendingNtfs.readStream: has stream")
var rcvCount = max(1, msgCount)
for await ntf in s {
nse.setBestAttemptNtf(showNotifications ? ntf : .empty)
rcvCount -= 1
if rcvCount == 0 || ntf.categoryIdentifier == ntfCategoryCallInvitation { break }
}
while !readCancelled {
dequeued = s.dequeue()
if let ntf = await dequeued?.task.value {
if readCancelled {
logger.debug("NotificationService PendingNtfs.readStream: read cancelled, put ntf to queue front")
s.frontEnqueue(ntf)
break
} else if case let .msgInfo(info) = ntf {
let found = expected.remove(info.msgId)
if found != nil {
logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty, privacy: .public)")
if expected.isEmpty { break }
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
s.frontEnqueue(ntf)
break
}
} else if ntfInfo.user.showNotifications {
logger.debug("NotificationService PendingNtfs.readStream: setting best attempt")
nse.setBestAttemptNtf(ntf)
if ntf.isCallInvitation { break }
}
} else {
break
}
}
nse.cancelRead = nil
logger.debug("NotificationService PendingNtfs.readStream: exiting")
logger.debug("PendingNtfs.readStream: exiting")
}
}
func writeStream(_ id: String, _ ntf: NSENotification) async {
logger.debug("NotificationService PendingNtfs.writeStream: \(id, privacy: .public)")
if let s = ntfStreams[id] {
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
s.enqueue(ntf)
}
}
}
// The current implementation assumes concurrent notification delivery and uses semaphores
// to process only one notification per connection (entity) at a time.
class NtfStreamSemaphores {
static let shared = NtfStreamSemaphores()
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-semaphores.lock")
private var semaphores: [String: DispatchSemaphore] = [:]
func waitForStream(_ id: String) {
streamSemaphore(id, value: 0)?.wait()
}
func signalStreamReady(_ id: String) {
streamSemaphore(id, value: 1)?.signal()
}
// this function returns nil if semaphore is just created, so passed value shoud be coordinated with the desired end value of the semaphore
private func streamSemaphore(_ id: String, value: Int) -> DispatchSemaphore? {
NtfStreamSemaphores.queue.sync {
if let s = semaphores[id] {
return s
} else {
semaphores[id] = DispatchSemaphore(value: value)
return nil
}
func writeStream(_ id: String, _ ntf: NSENotification) {
logger.debug("PendingNtfs.writeStream: \(id, privacy: .public)")
if let cont = ntfConts[id] {
logger.debug("PendingNtfs.writeStream: writing ntf")
cont.yield(ntf)
}
}
}
enum NSENotification {
case nse(UNMutableNotificationContent)
case callkit(RcvCallInvitation)
case nse(notification: UNMutableNotificationContent)
case callkit(invitation: RcvCallInvitation)
case empty
case msgInfo(NtfMsgInfo)
var isCallInvitation: Bool {
var categoryIdentifier: String? {
switch self {
case let .nse(ntf): ntf.categoryIdentifier == ntfCategoryCallInvitation
case .callkit: true
case .empty: false
case .msgInfo: false
case let .nse(ntf): return ntf.categoryIdentifier
case .callkit: return ntfCategoryCallInvitation
case .empty: return nil
}
}
}
// Once the last thread in the process completes processing chat controller is suspended, and the database is closed, to avoid
// background crashes and contention for database with the application (both UI and background fetch triggered either on schedule
// or when background notification is received.
class NSEThreads {
static let shared = NSEThreads()
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
private var allThreads: Set<UUID> = []
private var activeThreads: Set<UUID> = []
func newThread() -> UUID {
NSEThreads.queue.sync {
let (_, t) = allThreads.insert(UUID())
return t
}
}
func startThread(_ t: UUID) {
NSEThreads.queue.sync {
if allThreads.contains(t) {
_ = activeThreads.insert(t)
} else {
logger.warning("NotificationService startThread: thread \(t) was removed before it started")
}
}
}
func endThread(_ t: UUID) -> Bool {
NSEThreads.queue.sync {
let tActive = activeThreads.remove(t)
let t = allThreads.remove(t)
if tActive != nil && activeThreads.isEmpty {
return true
}
if t != nil && allThreads.isEmpty {
NSEChatState.shared.set(.suspended)
}
return false
}
}
var noThreads: Bool {
allThreads.isEmpty
}
}
// Notification service extension creates a new instance of the class and calls didReceive for each notification.
// Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never
// more than one process of notification service extension exists at a time.
// Soon after notification service delivers the last notification it is either suspended or terminated.
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptNtf: NSENotification?
var badgeCount: Int = 0
// thread is added to allThreads here - if thread did not start chat,
// chat does not need to be suspended but NSE state still needs to be set to "suspended".
var threadId: UUID? = NSEThreads.shared.newThread()
var receiveEntityId: String?
var cancelRead: (() -> Void)?
var appSubscriber: AppSubscriber?
var returnedSuspension = false
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
logger.debug("DEBUGGING: NotificationService.didReceive")
let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() }
setBestAttemptNtf(ntf)
if let ntf = request.content.mutableCopy() as? UNMutableNotificationContent {
setBestAttemptNtf(ntf)
}
self.contentHandler = contentHandler
registerGroupDefaults()
let appState = appStateGroupDefault.get()
logger.debug("NotificationService: app is \(appState.rawValue, privacy: .public)")
switch appState {
case .stopped:
setBadgeCount()
setBestAttemptNtf(createAppStoppedNtf())
deliverBestAttemptNtf()
case .suspended:
logger.debug("NotificationService: app is suspended")
setBadgeCount()
receiveNtfMessages(request, contentHandler)
case .suspending:
logger.debug("NotificationService: app is suspending")
setBadgeCount()
Task {
let state: AppState = await withCheckedContinuation { cont in
appSubscriber = appStateSubscriber { s in
if s == .suspended { appSuspension(s) }
}
DispatchQueue.global().asyncAfter(deadline: .now() + Double(appSuspendTimeout) + 1) {
logger.debug("NotificationService: appSuspension timeout")
appSuspension(appStateGroupDefault.get())
}
@Sendable
func appSuspension(_ s: AppState) {
if !self.returnedSuspension {
self.returnedSuspension = true
self.appSubscriber = nil // this disposes of appStateSubscriber
cont.resume(returning: s)
}
}
var state = appState
for _ in 1...5 {
_ = try await Task.sleep(nanoseconds: suspendingDelay)
state = appStateGroupDefault.get()
if state == .suspended || state != .suspending { break }
}
logger.debug("NotificationService: app state is now \(state.rawValue, privacy: .public)")
logger.debug("NotificationService: app state is \(state.rawValue, privacy: .public)")
if state.inactive {
receiveNtfMessages(request, contentHandler)
} else {
@@ -246,6 +106,7 @@ class NotificationService: UNNotificationServiceExtension {
}
}
default:
logger.debug("NotificationService: app state is \(appState.rawValue, privacy: .public)")
deliverBestAttemptNtf()
}
}
@@ -260,35 +121,27 @@ class NotificationService: UNNotificationServiceExtension {
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
let nonce = ntfData["nonce"] as? String,
let encNtfInfo = ntfData["message"] as? String,
// check it here again
appStateGroupDefault.get().inactive {
// thread is added to activeThreads tracking set here - if thread started chat it needs to be suspended
if let t = threadId { NSEThreads.shared.startThread(t) }
let dbStatus = startChat()
let dbStatus = startChat() {
if case .ok = dbStatus,
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)")
if let connEntity = ntfInfo.connEntity_ {
let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
if let connEntity = ntfMsgInfo.connEntity {
setBestAttemptNtf(
ntfInfo.ntfsEnabled
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity))
ntfMsgInfo.ntfsEnabled
? .nse(notification: createConnectionEventNtf(ntfMsgInfo.user, connEntity))
: .empty
)
if let id = connEntity.id {
receiveEntityId = id
NtfStreamSemaphores.shared.waitForStream(id)
if receiveEntityId != nil {
Task {
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
deliverBestAttemptNtf()
}
Task {
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count, showNotifications: ntfMsgInfo.user.showNotifications)
deliverBestAttemptNtf()
}
return
}
}
} else if let dbStatus = dbStatus {
return
} else {
setBestAttemptNtf(createErrorNtf(dbStatus))
}
}
@@ -306,14 +159,14 @@ class NotificationService: UNNotificationServiceExtension {
}
func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
setBestAttemptNtf(.nse(ntf))
setBestAttemptNtf(.nse(notification: ntf))
}
func setBestAttemptNtf(_ ntf: NSENotification) {
logger.debug("NotificationService.setBestAttemptNtf")
if case let .nse(notification) = ntf {
notification.badge = badgeCount as NSNumber
bestAttemptNtf = .nse(notification)
bestAttemptNtf = .nse(notification: notification)
} else {
bestAttemptNtf = ntf
}
@@ -321,41 +174,9 @@ class NotificationService: UNNotificationServiceExtension {
private func deliverBestAttemptNtf() {
logger.debug("NotificationService.deliverBestAttemptNtf")
if let cancel = cancelRead {
cancelRead = nil
cancel()
}
if let id = receiveEntityId {
receiveEntityId = nil
NtfStreamSemaphores.shared.signalStreamReady(id)
}
if let t = threadId {
threadId = nil
if NSEThreads.shared.endThread(t) {
logger.debug("NotificationService.deliverBestAttemptNtf: will suspend")
// suspension is delayed to allow chat core finalise any processing
// (e.g., send delivery receipts)
DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) {
if NSEThreads.shared.noThreads {
logger.debug("NotificationService.deliverBestAttemptNtf: suspending...")
suspendChat(nseSuspendTimeout)
}
}
}
}
if let handler = contentHandler, let ntf = bestAttemptNtf {
contentHandler = nil
bestAttemptNtf = nil
let deliver: (UNMutableNotificationContent?) -> Void = { ntf in
let useNtf = if let ntf = ntf {
appStateGroupDefault.get().running ? UNMutableNotificationContent() : ntf
} else {
UNMutableNotificationContent()
}
handler(useNtf)
}
switch ntf {
case let .nse(content): deliver(content)
case let .nse(content): handler(content)
case let .callkit(invitation):
CXProvider.reportNewIncomingVoIPPushPayload([
"displayName": invitation.contact.displayName,
@@ -363,201 +184,66 @@ class NotificationService: UNNotificationServiceExtension {
"media": invitation.callType.media.rawValue
]) { error in
if error == nil {
deliver(nil)
handler(UNMutableNotificationContent())
} else {
logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
deliver(createCallInvitationNtf(invitation))
logger.debug("reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
handler(createCallInvitationNtf(invitation))
}
}
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
case .empty: handler(UNMutableNotificationContent())
}
bestAttemptNtf = nil
}
}
}
// nseStateGroupDefault must not be used in NSE directly, only via this singleton
class NSEChatState {
static let shared = NSEChatState()
private var value_ = NSEState.created
var value: NSEState {
value_
}
func set(_ state: NSEState) {
nseStateGroupDefault.set(state)
sendNSEState(state)
value_ = state
}
init() {
// This is always set to .created state, as in case previous start of NSE crashed in .active state, it is stored correctly.
// Otherwise the app will be activating slower
set(.created)
}
}
var appSubscriber: AppSubscriber = appStateSubscriber { state in
logger.debug("NotificationService: appSubscriber")
if state.running && NSEChatState.shared.value.canSuspend {
logger.debug("NotificationService: appSubscriber app state \(state.rawValue), suspending")
suspendChat(nseSuspendTimeout)
}
}
func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber {
appMessageSubscriber { msg in
if case let .state(state) = msg {
logger.debug("NotificationService: appStateSubscriber \(state.rawValue, privacy: .public)")
onState(state)
}
}
}
var receiverStarted = false
let startLock = DispatchSemaphore(value: 1)
let suspendLock = DispatchSemaphore(value: 1)
var chatStarted = false
var networkConfig: NetCfg = getNetCfg()
let xftpConfig: XFTPFileConfig? = getXFTPCfg()
var xftpConfig: XFTPFileConfig? = getXFTPCfg()
// startChat uses semaphore startLock to ensure that only one didReceive thread can start chat controller
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
func startChat() -> DBMigrationResult? {
logger.debug("NotificationService: startChat")
if case .active = NSEChatState.shared.value { return .ok }
startLock.wait()
defer { startLock.signal() }
return switch NSEChatState.shared.value {
case .created: doStartChat()
case .starting: .ok // it should never get to this branch, as it would be waiting for start on startLock
case .active: .ok
case .suspending: activateChat()
case .suspended: activateChat()
}
}
func doStartChat() -> DBMigrationResult? {
logger.debug("NotificationService: doStartChat")
hs_init(0, nil)
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation(), backgroundMode: true)
if chatStarted { return .ok }
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation())
if dbStatus != .ok {
resetChatCtrl()
NSEChatState.shared.set(.created)
return dbStatus
}
let state = NSEChatState.shared.value
NSEChatState.shared.set(.starting)
if let user = apiGetActiveUser() {
logger.debug("NotificationService active user \(String(describing: user))")
logger.debug("active user \(String(describing: user))")
do {
try setNetworkConfig(networkConfig)
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(xftpConfig)
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
// prevent suspension while starting chat
suspendLock.wait()
defer { suspendLock.signal() }
if NSEChatState.shared.value == .starting {
updateNetCfg()
let justStarted = try apiStartChat()
NSEChatState.shared.set(.active)
if justStarted {
chatLastStartGroupDefault.set(Date.now)
Task {
if !receiverStarted {
receiverStarted = true
await receiveMessages()
}
}
}
return .ok
let justStarted = try apiStartChat()
chatStarted = true
if justStarted {
chatLastStartGroupDefault.set(Date.now)
Task { await receiveMessages() }
}
return .ok
} catch {
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
}
} else {
logger.debug("NotificationService: no active user")
logger.debug("no active user")
}
if NSEChatState.shared.value == .starting { NSEChatState.shared.set(state) }
return nil
}
func activateChat() -> DBMigrationResult? {
logger.debug("NotificationService: activateChat")
let state = NSEChatState.shared.value
NSEChatState.shared.set(.active)
if apiActivateChat() {
logger.debug("NotificationService: activateChat: after apiActivateChat")
return .ok
} else {
NSEChatState.shared.set(state)
return nil
}
}
// suspendChat uses semaphore suspendLock to ensure that only one suspension can happen.
func suspendChat(_ timeout: Int) {
logger.debug("NotificationService: suspendChat")
let state = NSEChatState.shared.value
if !state.canSuspend {
logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)")
} else {
suspendLock.wait()
defer { suspendLock.signal() }
NSEChatState.shared.set(.suspending)
if apiSuspendChat(timeoutMicroseconds: timeout * 1000000) {
logger.debug("NotificationService: suspendChat: after apiSuspendChat")
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: chatSuspended)
} else {
NSEChatState.shared.set(state)
}
}
}
func chatSuspended() {
logger.debug("NotificationService chatSuspended")
if case .suspending = NSEChatState.shared.value {
NSEChatState.shared.set(.suspended)
chatCloseStore()
logger.debug("NotificationService chatSuspended: suspended")
}
}
// A single loop is used per Notification service extension process to receive and process all messages depending on the NSE state
// If the extension is not active yet, or suspended/suspending, or the app is running, the notifications will no be received.
func receiveMessages() async {
logger.debug("NotificationService receiveMessages")
while true {
switch NSEChatState.shared.value {
// it should never get to "created" and "starting" branches, as NSE state is set to .active before the loop start
case .created: await delayWhenInactive()
case .starting: await delayWhenInactive()
case .active: await receiveMsg()
case .suspending: await receiveMsg()
case .suspended: await delayWhenInactive()
}
}
func receiveMsg() async {
updateNetCfg()
if let msg = await chatRecvMsg() {
logger.debug("NotificationService receiveMsg: message")
if let (id, ntf) = await receivedMsgNtf(msg) {
logger.debug("NotificationService receiveMsg: notification")
await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.writeStream(id, ntf)
}
}
}
func delayWhenInactive() async {
logger.debug("NotificationService delayWhenInactive")
_ = try? await Task.sleep(nanoseconds: 1000_000000)
}
}
func chatRecvMsg() async -> ChatResponse? {
@@ -571,14 +257,14 @@ private let isInChina = SKStorefront().countryCode == "CHN"
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)")
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
switch res {
case let .contactConnected(user, contact, _):
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
return (contact.id, .nse(notification: createContactConnectedNtf(user, contact)))
// case let .contactConnecting(contact):
// TODO profile update
case let .receivedContactRequest(user, contactRequest):
return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest)))
return (UserContact(contactRequest: contactRequest).id, .nse(notification: createContactRequestNtf(user, contactRequest)))
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
var cItem = aChatItem.chatItem
@@ -588,7 +274,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
if let file = cItem.autoReceiveFile() {
cItem = autoReceiveFile(file, encrypted: cItem.encryptLocalFile) ?? cItem
}
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty
return cItem.showNotification ? (aChatItem.chatId, ntf) : nil
case let .rcvFileSndCancelled(_, aChatItem, _):
cleanupFile(aChatItem)
@@ -606,15 +292,10 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
return (
invitation.contact.id,
useCallKit() ? .callkit(invitation) : .nse(createCallInvitationNtf(invitation))
useCallKit() ? .callkit(invitation: invitation) : .nse(notification: createCallInvitationNtf(invitation))
)
case let .ntfMessage(_, connEntity, ntfMessage):
return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil }
case .chatSuspended:
chatSuspended()
return nil
default:
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
logger.debug("NotificationService processReceivedMsg ignored event: \(res.responseType)")
return nil
}
}
@@ -653,21 +334,6 @@ func apiStartChat() throws -> Bool {
}
}
func apiActivateChat() -> Bool {
chatReopenStore()
let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false))
if case .cmdOk = r { return true }
logger.error("NotificationService apiActivateChat error: \(String(describing: r))")
return false
}
func apiSuspendChat(timeoutMicroseconds: Int) -> Bool {
let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
if case .cmdOk = r { return true }
logger.error("NotificationService apiSuspendChat error: \(String(describing: r))")
return false
}
func apiSetTempFolder(tempFolder: String) throws {
let r = sendSimpleXCmd(.setTempFolder(tempFolder: tempFolder))
if case .cmdOk = r { return }
@@ -698,8 +364,8 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
return nil
}
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user {
return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages)
if case let .ntfMessages(user, connEntity, msgTs, ntfMessages) = r, let user = user {
return NtfMessages(user: user, connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
} else if case let .chatCmdError(_, error) = r {
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
} else {
@@ -739,11 +405,11 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
struct NtfMessages {
var user: User
var connEntity_: ConnectionEntity?
var connEntity: ConnectionEntity?
var msgTs: Date?
var ntfMessages: [NtfMsgInfo]
var ntfsEnabled: Bool {
user.showNotifications && (connEntity_?.ntfsEnabled ?? false)
user.showNotifications && (connEntity?.ntfsEnabled ?? false)
}
}
+56 -78
View File
@@ -16,6 +16,7 @@
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415323A4082FC92887F906 /* WebRTCClient.swift */; };
18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */; };
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */; };
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; };
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00164328A26FBC0094D739 /* ContextMenu.swift */; };
@@ -38,15 +39,9 @@
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C36027227F47AD5009F19D9 /* AppDelegate.swift */; };
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; };
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; };
5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */; };
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
5C4E80E42B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */; };
5C4E80E52B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */; };
5C4E80E62B40A96C0080FAE2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E12B40A96C0080FAE2 /* libgmp.a */; };
5C4E80E72B40A96C0080FAE2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */; };
5C4E80E82B40A96C0080FAE2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4E80E32B40A96C0080FAE2 /* libffi.a */; };
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
@@ -60,6 +55,7 @@
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */; };
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; };
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; };
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */; };
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
@@ -96,6 +92,8 @@
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; };
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; };
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2084E28DA4B4800D024EC /* RTCServers.swift */; };
5CB2085128DB64CA00D024EC /* CreateLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2085028DB64CA00D024EC /* CreateLinkView.swift */; };
5CB2085328DB7CAF00D024EC /* ConnectViaLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */; };
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; };
@@ -117,8 +115,8 @@
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -144,15 +142,15 @@
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */; };
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
5CF4DF772AFF8D4E007893ED /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF722AFF8D4D007893ED /* libffi.a */; };
5CF4DF782AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF732AFF8D4D007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */; };
5CF4DF792AFF8D4E007893ED /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF742AFF8D4D007893ED /* libgmpxx.a */; };
5CF4DF7A2AFF8D4E007893ED /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF752AFF8D4E007893ED /* libgmp.a */; };
5CF4DF7B2AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF762AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */; };
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; };
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; };
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
@@ -184,7 +182,6 @@
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
@@ -259,6 +256,7 @@
18415B08031E8FB0F7FC27F9 /* CallViewRenderers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallViewRenderers.swift; sourceTree = "<group>"; };
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = "<group>"; };
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = "<group>"; };
@@ -284,16 +282,10 @@
5C36027227F47AD5009F19D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectDesktopView.swift; sourceTree = "<group>"; };
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrityErrorItemView.swift; sourceTree = "<group>"; };
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a"; sourceTree = "<group>"; };
5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a"; sourceTree = "<group>"; };
5C4E80E12B40A96C0080FAE2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C4E80E32B40A96C0080FAE2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
@@ -319,6 +311,7 @@
5C65DAED29CB8908003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperView.swift; sourceTree = "<group>"; };
5C65F341297D3F3600B67AF3 /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; };
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
5C6BA666289BD954009B8ECC /* DismissSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissSheets.swift; sourceTree = "<group>"; };
5C6D183229E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = "pl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C6D183329E93FBA00D430B3 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -375,6 +368,8 @@
5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; };
5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; };
5CB2084E28DA4B4800D024EC /* RTCServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCServers.swift; sourceTree = "<group>"; };
5CB2085028DB64CA00D024EC /* CreateLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateLinkView.swift; sourceTree = "<group>"; };
5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViaLinkView.swift; sourceTree = "<group>"; };
5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
@@ -400,8 +395,8 @@
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -428,14 +423,14 @@
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentQueue.swift; sourceTree = "<group>"; };
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
5CF4DF722AFF8D4D007893ED /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CF4DF732AFF8D4D007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a"; sourceTree = "<group>"; };
5CF4DF742AFF8D4D007893ED /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CF4DF752AFF8D4E007893ED /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CF4DF762AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a"; sourceTree = "<group>"; };
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = "<group>"; };
640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
@@ -469,7 +464,6 @@
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -511,13 +505,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C4E80E72B40A96C0080FAE2 /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C4E80E62B40A96C0080FAE2 /* libgmp.a in Frameworks */,
5CF4DF792AFF8D4E007893ED /* libgmpxx.a in Frameworks */,
5CF4DF772AFF8D4E007893ED /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C4E80E82B40A96C0080FAE2 /* libffi.a in Frameworks */,
5C4E80E52B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a in Frameworks */,
5C4E80E42B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a in Frameworks */,
5CF4DF7A2AFF8D4E007893ED /* libgmp.a in Frameworks */,
5CF4DF7B2AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a in Frameworks */,
5CF4DF782AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -550,7 +544,6 @@
5CB924DD27A8622200ACCCDD /* NewChat */,
5CFA59C22860B04D00863A68 /* Database */,
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
5CA8D01B2AD9B076001FD661 /* RemoteAccess */,
5CB924DF27A8678B00ACCCDD /* UserSettings */,
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
);
@@ -579,11 +572,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5C4E80E32B40A96C0080FAE2 /* libffi.a */,
5C4E80E12B40A96C0080FAE2 /* libgmp.a */,
5C4E80E22B40A96C0080FAE2 /* libgmpxx.a */,
5C4E80E02B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ-ghc9.6.3.a */,
5C4E80DF2B40A96C0080FAE2 /* libHSsimplex-chat-5.5.0.0-FwZXD1cMpkc1VLQMq43OyQ.a */,
5CF4DF722AFF8D4D007893ED /* libffi.a */,
5CF4DF752AFF8D4E007893ED /* libgmp.a */,
5CF4DF742AFF8D4D007893ED /* libgmpxx.a */,
5CF4DF732AFF8D4D007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */,
5CF4DF762AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -608,7 +601,6 @@
5C35CFC727B2782E00FB6C6D /* BGManager.swift */,
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */,
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */,
5CF937212B25034A00E1D781 /* NSESubscriber.swift */,
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */,
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */,
5CBD2859295711D700EC2CF4 /* ImageUtils.swift */,
@@ -636,7 +628,6 @@
64466DCB29FFE3E800E3D48D /* MailView.swift */,
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */,
8C05382D2B39887E006436DC /* VideoUtils.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -693,14 +684,6 @@
path = "Tests iOS";
sourceTree = "<group>";
};
5CA8D01B2AD9B076001FD661 /* RemoteAccess */ = {
isa = PBXGroup;
children = (
5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */,
);
path = RemoteAccess;
sourceTree = "<group>";
};
5CB0BA8C282711BC00B3292C /* Onboarding */ = {
isa = PBXGroup;
children = (
@@ -729,10 +712,14 @@
5CB924DD27A8622200ACCCDD /* NewChat */ = {
isa = PBXGroup;
children = (
640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */,
640417CC2B29B8C200CCB412 /* NewChatView.swift */,
5C6AD81227A834E300348BD7 /* NewChatButton.swift */,
5CCD403327A5F6DF00368C90 /* AddContactView.swift */,
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */,
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */,
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */,
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */,
5CB2085028DB64CA00D024EC /* CreateLinkView.swift */,
5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */,
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */,
);
path = NewChat;
@@ -786,7 +773,6 @@
isa = PBXGroup;
children = (
5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */,
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */,
5CDCAD472818589900503DA2 /* NotificationService.swift */,
5CDCAD492818589900503DA2 /* Info.plist */,
5CB0BA862826CB3A00B3292C /* InfoPlist.strings */,
@@ -807,13 +793,10 @@
64DAE1502809D9F5000DA960 /* FileUtils.swift */,
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */,
5C00168028C4FE760094D739 /* KeyChain.swift */,
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */,
5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */,
5CE2BA8A2845332200EC33A6 /* SimpleX.h */,
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */,
5CE2BA96284537A800EC33A6 /* dummy.m */,
5CD67B8D2B0E858A00C510B1 /* hs_init.h */,
5CD67B8E2B0E858A00C510B1 /* hs_init.c */,
);
path = SimpleXChat;
sourceTree = "<group>";
@@ -898,7 +881,6 @@
buildActionMask = 2147483647;
files = (
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */,
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */,
5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1101,8 +1083,8 @@
buildActionMask = 2147483647;
files = (
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */,
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */,
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */,
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */,
@@ -1149,11 +1131,13 @@
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */,
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */,
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */,
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */,
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */,
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */,
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */,
5CB2085128DB64CA00D024EC /* CreateLinkView.swift in Sources */,
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */,
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */,
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */,
@@ -1162,8 +1146,8 @@
5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */,
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */,
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */,
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */,
640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */,
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */,
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */,
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */,
@@ -1178,7 +1162,6 @@
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */,
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */,
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */,
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */,
@@ -1187,7 +1170,6 @@
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */,
5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */,
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */,
5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */,
5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */,
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */,
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
@@ -1198,9 +1180,9 @@
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */,
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */,
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */,
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */,
@@ -1220,6 +1202,7 @@
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */,
5CB2085328DB7CAF00D024EC /* ConnectViaLinkView.swift in Sources */,
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */,
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
@@ -1257,7 +1240,6 @@
files = (
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */,
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */,
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1265,11 +1247,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */,
5C00168128C4FE760094D739 /* KeyChain.swift in Sources */,
5CE2BA97284537A800EC33A6 /* dummy.m in Sources */,
5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */,
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */,
5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */,
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */,
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */,
@@ -1502,7 +1482,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 181;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1510,7 +1490,6 @@
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
INFOPLIST_KEY_NSFaceIDUsageDescription = "SimpleX uses Face ID for local authentication";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SimpleX needs access to Photo Library for saving captured and received media";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -1524,7 +1503,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.5;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1545,7 +1524,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 181;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1553,7 +1532,6 @@
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
INFOPLIST_KEY_NSFaceIDUsageDescription = "SimpleX uses Face ID for local authentication";
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SimpleX needs access to Photo Library for saving captured and received media";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -1567,7 +1545,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.5;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1626,7 +1604,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 181;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1639,7 +1617,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.5;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1658,7 +1636,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 181;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1671,7 +1649,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.5;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1690,7 +1668,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 181;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1714,7 +1692,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.5;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1736,7 +1714,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 187;
CURRENT_PROJECT_VERSION = 181;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1760,7 +1738,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.5;
MARKETING_VERSION = 5.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;

Some files were not shown because too many files have changed in this diff Show More